From be8c19e483c7fca17f64510d1713ce5228a58843 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 29 Apr 2026 12:34:52 -0700 Subject: [PATCH 01/55] [release/13.3] Stabilizing builds in preparation for 13.3 release (#16566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [release/13.3] Stabilizing builds in preparation for 13.3 release - Flip StabilizePackageVersion default to true in eng/Versions.props - Drop the compute_version_suffix step from ci.yml so PR builds use the stabilized version - Pin Aspire.AppHost.Sdk import to 13.3.0 in RepoTesting.targets - Pin all stabilizable Aspire packages to 13.3.0 in Directory.Packages.Helix.props (packages whose csproj sets SuppressFinalPackageVersion=true keep $(PackageVersion)) - Skip preview-only AppHosts (Kusto, Foundry, Keycloak, Kubernetes, Maui) in the TypeScript polyglot validation script (see #15335) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [release/13.3] Skip tests blocked by suppressed-package stabilization gap Two test buckets cannot run on a stabilized branch because they exercise NuGet restore against Aspire.Hosting.* packages that have SuppressFinalPackageVersion=true and therefore only ship as prerelease (e.g. 13.3.0-dev.) — but the stabilized build asks for >= 13.3.0 stable. Polyglot SDK Validation: - Add 'Aspire.Hosting' to the skip list in test-typescript-playground.sh (it transitively depends on Aspire.Hosting.Azure.Kubernetes via aspire.config.json). - Apply the same skip block to test-python-playground.sh and test-java-playground.sh (the 13.2 PR only updated the TypeScript script). Cli.EndToEnd-KubernetesDeploy* (11 files): - Mark each [Fact] with [ActiveIssue(#15335)]. The proper fix is the dynamic version computation tracked by #15335 (PR #15681). When that lands the ActiveIssue tags can be removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 28 ---- .../test-java-playground.sh | 36 ++++- .../test-python-playground.sh | 36 ++++- .../test-typescript-playground.sh | 34 ++++- eng/Versions.props | 2 +- .../KubernetesDeployBasicApiServiceTests.cs | 1 + .../KubernetesDeployTypeScriptTests.cs | 1 + .../KubernetesDeployWithGarnetTests.cs | 1 + .../KubernetesDeployWithMongoDBTests.cs | 1 + .../KubernetesDeployWithMySqlTests.cs | 1 + .../KubernetesDeployWithNatsTests.cs | 1 + .../KubernetesDeployWithPostgresTests.cs | 1 + .../KubernetesDeployWithRabbitMQTests.cs | 1 + .../KubernetesDeployWithRedisTests.cs | 1 + .../KubernetesDeployWithSqlServerTests.cs | 1 + .../KubernetesDeployWithValkeyTests.cs | 1 + .../RepoTesting/Aspire.RepoTesting.targets | 4 +- .../Directory.Packages.Helix.props | 142 +++++++++--------- 18 files changed, 188 insertions(+), 105 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb20ddcfd8..d21795d872a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,8 +23,6 @@ jobs: if: ${{ github.repository_owner == 'microsoft' }} outputs: skip_workflow: ${{ (steps.check_for_changes.outputs.no_changes == 'true' || steps.check_for_changes.outputs.only_changed == 'true') && 'true' || 'false' }} - VERSION_SUFFIX_OVERRIDE: ${{ steps.compute_version_suffix.outputs.VERSION_SUFFIX_OVERRIDE }} - EXTENSION_VERSION_OVERRIDE: ${{ steps.compute_version_suffix.outputs.EXTENSION_VERSION_OVERRIDE }} steps: - name: Checkout code @@ -40,37 +38,11 @@ jobs: with: patterns_file: eng/testing/github-ci-trigger-patterns.txt - - id: compute_version_suffix - name: Compute version suffix for PRs - if: ${{ github.event_name == 'pull_request' }} - shell: pwsh - env: - # Use the pull request head SHA instead of GITHUB_SHA (which can be a merge commit) - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_NUMBER: ${{ github.event.number }} - run: | - Write-Host "Determining VERSION_SUFFIX_OVERRIDE (PR only step)..." - if ([string]::IsNullOrWhiteSpace($Env:PR_HEAD_SHA)) { - Write-Error "PR_HEAD_SHA not set; cannot compute version suffix." - exit 1 - } - $SHORT_SHA = $Env:PR_HEAD_SHA.Substring(0,8) - $VERSION_SUFFIX = "/p:VersionSuffix=pr.$($Env:PR_NUMBER).g$SHORT_SHA" - Write-Host "Computed VERSION_SUFFIX_OVERRIDE=$VERSION_SUFFIX" - "VERSION_SUFFIX_OVERRIDE=$VERSION_SUFFIX" | Out-File -FilePath $Env:GITHUB_OUTPUT -Append -Encoding utf8 - - $EXTENSION_VERSION = "99.0.$($Env:PR_NUMBER)" - Write-Host "Computed EXTENSION_VERSION_OVERRIDE=$EXTENSION_VERSION" - "EXTENSION_VERSION_OVERRIDE=$EXTENSION_VERSION" | Out-File -FilePath $Env:GITHUB_OUTPUT -Append -Encoding utf8 - tests: uses: ./.github/workflows/tests.yml name: Tests needs: [prepare_for_ci] if: ${{ github.repository_owner == 'microsoft' && needs.prepare_for_ci.outputs.skip_workflow != 'true' }} - with: - versionOverrideArg: ${{ needs.prepare_for_ci.outputs.VERSION_SUFFIX_OVERRIDE }} - extensionVersionOverride: ${{ needs.prepare_for_ci.outputs.EXTENSION_VERSION_OVERRIDE }} # This job is used for branch protection. It fails if any of the dependent jobs failed results: diff --git a/.github/workflows/polyglot-validation/test-java-playground.sh b/.github/workflows/polyglot-validation/test-java-playground.sh index c71b377d800..c06ef888ab1 100644 --- a/.github/workflows/polyglot-validation/test-java-playground.sh +++ b/.github/workflows/polyglot-validation/test-java-playground.sh @@ -57,10 +57,44 @@ echo "" FAILED=() PASSED=() +SKIPPED=() + +# Packages that only produce prerelease versions (SuppressFinalPackageVersion=true) cannot be +# restored when the build is stabilized, because aspire restore requests stable versions. +# Skip these until the build infrastructure dynamically computes versions. See #15335. +# The "Aspire.Hosting" entry is included because its aspire.config.json transitively depends on +# Aspire.Hosting.Azure.Kubernetes, which is suppressed. +SKIP_PREVIEW_ONLY=( + "Aspire.Hosting" + "Aspire.Hosting.Azure.Kusto" + "Aspire.Hosting.Foundry" + "Aspire.Hosting.Keycloak" + "Aspire.Hosting.Kubernetes" + "Aspire.Hosting.Maui" +) for app_dir in "${APP_DIRS[@]}"; do integration_name="$(basename "$(dirname "$app_dir")")" + # Check if this integration is in the skip list + skip=false + for skip_pkg in "${SKIP_PREVIEW_ONLY[@]}"; do + if [ "$integration_name" = "$skip_pkg" ]; then + skip=true + break + fi + done + + if [ "$skip" = true ]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Testing: $integration_name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " ⏭ Skipping (preview-only package, see #15335)" + SKIPPED+=("$integration_name") + echo "" + continue + fi + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Testing: $integration_name" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -104,7 +138,7 @@ done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed out of ${#APP_DIRS[@]} AppHosts" +echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed, ${#SKIPPED[@]} skipped out of ${#APP_DIRS[@]} AppHosts" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ ${#FAILED[@]} -gt 0 ]; then diff --git a/.github/workflows/polyglot-validation/test-python-playground.sh b/.github/workflows/polyglot-validation/test-python-playground.sh index aa0dc30d00b..a5b73f64c2f 100755 --- a/.github/workflows/polyglot-validation/test-python-playground.sh +++ b/.github/workflows/polyglot-validation/test-python-playground.sh @@ -50,10 +50,44 @@ echo "" FAILED=() PASSED=() +SKIPPED=() + +# Packages that only produce prerelease versions (SuppressFinalPackageVersion=true) cannot be +# restored when the build is stabilized, because aspire restore requests stable versions. +# Skip these until the build infrastructure dynamically computes versions. See #15335. +# The "Aspire.Hosting" entry is included because its aspire.config.json transitively depends on +# Aspire.Hosting.Azure.Kubernetes, which is suppressed. +SKIP_PREVIEW_ONLY=( + "Aspire.Hosting" + "Aspire.Hosting.Azure.Kusto" + "Aspire.Hosting.Foundry" + "Aspire.Hosting.Keycloak" + "Aspire.Hosting.Kubernetes" + "Aspire.Hosting.Maui" +) for app_dir in "${APP_DIRS[@]}"; do integration_name="$(basename "$(dirname "$app_dir")")" + # Check if this integration is in the skip list + skip=false + for skip_pkg in "${SKIP_PREVIEW_ONLY[@]}"; do + if [ "$integration_name" = "$skip_pkg" ]; then + skip=true + break + fi + done + + if [ "$skip" = true ]; then + echo "----------------------------------------" + echo "Testing: $integration_name" + echo "----------------------------------------" + echo " -> Skipping (preview-only package, see #15335)" + SKIPPED+=("$integration_name") + echo "" + continue + fi + echo "----------------------------------------" echo "Testing: $integration_name" echo "----------------------------------------" @@ -98,7 +132,7 @@ done echo "" echo "----------------------------------------" -echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed out of ${#APP_DIRS[@]} AppHosts" +echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed, ${#SKIPPED[@]} skipped out of ${#APP_DIRS[@]} AppHosts" echo "----------------------------------------" if [ ${#FAILED[@]} -gt 0 ]; then diff --git a/.github/workflows/polyglot-validation/test-typescript-playground.sh b/.github/workflows/polyglot-validation/test-typescript-playground.sh index 9a37073bcca..20f8a55945b 100644 --- a/.github/workflows/polyglot-validation/test-typescript-playground.sh +++ b/.github/workflows/polyglot-validation/test-typescript-playground.sh @@ -50,10 +50,42 @@ echo "" FAILED=() PASSED=() +SKIPPED=() + +# Packages that only produce prerelease versions (SuppressFinalPackageVersion=true) cannot be +# restored when the build is stabilized, because aspire restore requests stable versions. +# Skip these until the build infrastructure dynamically computes versions. See #15335. +SKIP_PREVIEW_ONLY=( + "Aspire.Hosting" + "Aspire.Hosting.Azure.Kusto" + "Aspire.Hosting.Foundry" + "Aspire.Hosting.Keycloak" + "Aspire.Hosting.Kubernetes" + "Aspire.Hosting.Maui" +) for app_dir in "${APP_DIRS[@]}"; do integration_name="$(basename "$(dirname "$app_dir")")" + # Check if this integration is in the skip list + skip=false + for skip_pkg in "${SKIP_PREVIEW_ONLY[@]}"; do + if [ "$integration_name" = "$skip_pkg" ]; then + skip=true + break + fi + done + + if [ "$skip" = true ]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Testing: $integration_name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " ⏭ Skipping (preview-only package, see #15335)" + SKIPPED+=("$integration_name") + echo "" + continue + fi + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Testing: $integration_name" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -93,7 +125,7 @@ done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed out of ${#APP_DIRS[@]} AppHosts" +echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed, ${#SKIPPED[@]} skipped out of ${#APP_DIRS[@]} AppHosts" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ ${#FAILED[@]} -gt 0 ]; then diff --git a/eng/Versions.props b/eng/Versions.props index b0841e52146..dc5befc4702 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -21,7 +21,7 @@ 2.2.1 17.14.1 - false + true release diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs index d533effc530..dfc00e3d98c 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployBasicApiServiceTests(ITestOutputHelper outpu private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sBasicApiService() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs index b7912b4284c..33ca928cb76 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs @@ -17,6 +17,7 @@ public sealed class KubernetesDeployTypeScriptTests(ITestOutputHelper output) private const string ProjectName = "K8sTsTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployTypeScriptAppToKubernetes() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs index 07ffc696c85..1cf7f803b98 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithGarnetTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithGarnet() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs index b83c7290738..e7441312407 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithMongoDBTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithMongoDB() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs index f097f0036be..62ea1454cdf 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithMySqlTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithMySql() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs index e9f751454c1..d3b02a49261 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs @@ -17,6 +17,7 @@ public sealed class KubernetesDeployWithNatsTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] [QuarantinedTest("https://github.com/microsoft/aspire/issues/15789")] public async Task DeployK8sWithNats() diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs index c824c32e840..0a88024eaf9 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithPostgresTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithPostgres() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs index 739c3b57eb6..29d1f9fee09 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithRabbitMQTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithRabbitMQ() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs index 3dda99453f0..f6a86ca6a36 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithRedisTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithRedis() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs index c9c1a467019..a4dda30ea65 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithSqlServerTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithSqlServer() { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs index fa6aadbce44..eff86e75159 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs @@ -16,6 +16,7 @@ public sealed class KubernetesDeployWithValkeyTests(ITestOutputHelper output) private const string ProjectName = "K8sDeployTest"; [Fact] + [ActiveIssue("https://github.com/microsoft/aspire/issues/15335")] [CaptureWorkspaceOnFailure] public async Task DeployK8sWithValkey() { diff --git a/tests/Shared/RepoTesting/Aspire.RepoTesting.targets b/tests/Shared/RepoTesting/Aspire.RepoTesting.targets index 67918e1fb52..a6a5296a7fc 100644 --- a/tests/Shared/RepoTesting/Aspire.RepoTesting.targets +++ b/tests/Shared/RepoTesting/Aspire.RepoTesting.targets @@ -33,7 +33,7 @@ `AspireProjectOrPackageReference` - maps to projects in `src/` or `src/Components/` --> - + @@ -165,6 +165,6 @@ $(MajorVersion).$(MinorVersion).$(PatchVersion) - + diff --git a/tests/Shared/RepoTesting/Directory.Packages.Helix.props b/tests/Shared/RepoTesting/Directory.Packages.Helix.props index 5428992b1ba..03d9d7bc9fe 100644 --- a/tests/Shared/RepoTesting/Directory.Packages.Helix.props +++ b/tests/Shared/RepoTesting/Directory.Packages.Helix.props @@ -3,81 +3,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + From ab9c135d7fdfc56f681bfd51f06aedc14aafa5eb Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 1 May 2026 00:59:16 +0800 Subject: [PATCH 02/55] Remove MarkupString from SpanDetails and StructuredLogDetails resource strings (#16584) Replace HTML-embedded resource strings with plain label strings and move the formatting into Razor markup directly. This eliminates the need for MarkupString casts and string.Format for these toolbar items. --- .../Components/Controls/SpanDetails.razor | 6 +-- .../Controls/StructuredLogDetails.razor | 4 +- .../Resources/ControlsStrings.Designer.cs | 30 ++++++------- .../Resources/ControlsStrings.resx | 25 +++++------ .../Resources/xlf/ControlsStrings.cs.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.de.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.es.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.fr.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.it.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.ja.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.ko.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.pl.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.pt-BR.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.ru.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.tr.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.zh-Hans.xlf | 44 +++++++++---------- .../Resources/xlf/ControlsStrings.zh-Hant.xlf | 44 +++++++++---------- 17 files changed, 316 insertions(+), 321 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor b/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor index 207e2c59031..9cd43a3e7ee 100644 --- a/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor +++ b/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor @@ -8,11 +8,11 @@ {
- @((MarkupString)string.Format(ControlsStrings.SpanDetailsResource, ViewModel.Span.Source.Resource.ResourceName)) + @Loc[nameof(ControlsStrings.SpanDetailsResourceLabel)] @ViewModel.Span.Source.Resource.ResourceName
- @((MarkupString)string.Format(ControlsStrings.SpanDetailsDuration, DurationFormatter.FormatDuration(ViewModel.Span.Duration, CultureInfo.CurrentCulture))) + @Loc[nameof(ControlsStrings.SpanDetailsDurationLabel)] @DurationFormatter.FormatDuration(ViewModel.Span.Duration, CultureInfo.CurrentCulture)
@@ -21,7 +21,7 @@ var startTime = ViewModel.Span.StartTime - ViewModel.Span.Trace.FirstSpan.StartTime; var formattedStartTime = startTime > TimeSpan.Zero ? DurationFormatter.FormatDuration(startTime, CultureInfo.CurrentCulture) : $"0{DurationFormatter.GetUnit(ViewModel.Span.Duration)}"; } - @((MarkupString)string.Format(ControlsStrings.SpanDetailsStartTime, formattedStartTime)) + @Loc[nameof(ControlsStrings.SpanDetailsStartTimeLabel)] @formattedStartTime
- @((MarkupString)string.Format(ControlsStrings.StructuredLogsDetailsResource, ViewModel.LogEntry.ResourceView.Resource.ResourceName)) + @Loc[nameof(ControlsStrings.StructuredLogsDetailsResourceLabel)] @ViewModel.LogEntry.ResourceView.Resource.ResourceName
- @((MarkupString)string.Format(ControlsStrings.StructuredLogsDetailsTimestamp, FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, ViewModel.LogEntry.TimeStamp, MillisecondsDisplay.Truncated))) + @Loc[nameof(ControlsStrings.StructuredLogsDetailsTimestampLabel)] @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, ViewModel.LogEntry.TimeStamp, MillisecondsDisplay.Truncated)
- /// Looks up a localized string similar to Duration <strong>{0}</strong>. + /// Looks up a localized string similar to Duration. /// - public static string SpanDetailsDuration { + public static string SpanDetailsDurationLabel { get { - return ResourceManager.GetString("SpanDetailsDuration", resourceCulture); + return ResourceManager.GetString("SpanDetailsDurationLabel", resourceCulture); } } @@ -1006,11 +1006,11 @@ public static string SpanDetailsLinksHeader { } /// - /// Looks up a localized string similar to Resource <strong>{0}</strong>. + /// Looks up a localized string similar to Resource. /// - public static string SpanDetailsResource { + public static string SpanDetailsResourceLabel { get { - return ResourceManager.GetString("SpanDetailsResource", resourceCulture); + return ResourceManager.GetString("SpanDetailsResourceLabel", resourceCulture); } } @@ -1051,11 +1051,11 @@ public static string SpanDetailsSpanPrefix { } /// - /// Looks up a localized string similar to Start time <strong>{0}</strong>. + /// Looks up a localized string similar to Start time. /// - public static string SpanDetailsStartTime { + public static string SpanDetailsStartTimeLabel { get { - return ResourceManager.GetString("SpanDetailsStartTime", resourceCulture); + return ResourceManager.GetString("SpanDetailsStartTimeLabel", resourceCulture); } } @@ -1150,11 +1150,11 @@ public static string StructuredLogsDetailsLogEntryHeader { } /// - /// Looks up a localized string similar to Resource <strong>{0}</strong>. + /// Looks up a localized string similar to Resource. /// - public static string StructuredLogsDetailsResource { + public static string StructuredLogsDetailsResourceLabel { get { - return ResourceManager.GetString("StructuredLogsDetailsResource", resourceCulture); + return ResourceManager.GetString("StructuredLogsDetailsResourceLabel", resourceCulture); } } @@ -1168,11 +1168,11 @@ public static string StructuredLogsDetailsResourceHeader { } /// - /// Looks up a localized string similar to Timestamp <strong>{0}</strong>. + /// Looks up a localized string similar to Timestamp. /// - public static string StructuredLogsDetailsTimestamp { + public static string StructuredLogsDetailsTimestampLabel { get { - return ResourceManager.GetString("StructuredLogsDetailsTimestamp", resourceCulture); + return ResourceManager.GetString("StructuredLogsDetailsTimestampLabel", resourceCulture); } } diff --git a/src/Aspire.Dashboard/Resources/ControlsStrings.resx b/src/Aspire.Dashboard/Resources/ControlsStrings.resx index 64599e3e356..192ace96ffc 100644 --- a/src/Aspire.Dashboard/Resources/ControlsStrings.resx +++ b/src/Aspire.Dashboard/Resources/ControlsStrings.resx @@ -192,17 +192,14 @@ State - - Resource <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified + + Resource - - Duration <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration - - Start time <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time View logs @@ -286,13 +283,11 @@ Loading... - - Resource <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified + + Resource - - Timestamp <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Timestamp URLs diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf index 6cf634ebaf9..031323983e8 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf @@ -512,10 +512,10 @@ Podrobnosti - - Duration <strong>{0}</strong> - Doba trvání: <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Odkazy - - Resource <strong>{0}</strong> - Prostředek <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Prostředek + + Resource + Resource + + Span Rozpětí @@ -552,10 +552,10 @@ Rozpětí - - Start time <strong>{0}</strong> - Čas spuštění: <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Položka protokolu - - Resource <strong>{0}</strong> - Prostředek <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Prostředek - - Timestamp <strong>{0}</strong> - Časové razítko <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf index d2828a0e5ec..6550c066036 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf @@ -512,10 +512,10 @@ Details - - Duration <strong>{0}</strong> - Dauer <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Links - - Resource <strong>{0}</strong> - Ressource <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Ressource + + Resource + Resource + + Span Span @@ -552,10 +552,10 @@ Span - - Start time <strong>{0}</strong> - Startzeit <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Protokolleintrag - - Resource <strong>{0}</strong> - Ressource <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Ressource - - Timestamp <strong>{0}</strong> - Zeitstempel <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf index d56e8237693..c08dce48d8d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf @@ -512,10 +512,10 @@ Detalles - - Duration <strong>{0}</strong> - Duración <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Vínculos - - Resource <strong>{0}</strong> - Recurso <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Recurso + + Resource + Resource + + Span Span @@ -552,10 +552,10 @@ Span - - Start time <strong>{0}</strong> - Hora de inicio <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Entrada del registro - - Resource <strong>{0}</strong> - Recurso <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Recurso - - Timestamp <strong>{0}</strong> - Marca de tiempo <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf index a08d31d51ed..3d3cfdb16d0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf @@ -512,10 +512,10 @@ Détails - - Duration <strong>{0}</strong> - Durée <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Liens - - Resource <strong>{0}</strong> - Ressource <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Ressource + + Resource + Resource + + Span Étendue @@ -552,10 +552,10 @@ Étendue - - Start time <strong>{0}</strong> - <strong>Heure de début : </strong> {0} - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Entrée de journal - - Resource <strong>{0}</strong> - Ressource <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Ressource - - Timestamp <strong>{0}</strong> - Timestamp <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf index 79514ad22a5..46a247c4bb5 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf @@ -512,10 +512,10 @@ Dettagli - - Duration <strong>{0}</strong> - Durata <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Collegamenti - - Resource <strong>{0}</strong> - Risorsa <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Risorsa + + Resource + Resource + + Span Span @@ -552,10 +552,10 @@ Span - - Start time <strong>{0}</strong> - Ora di inizio <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Voce di log - - Resource <strong>{0}</strong> - Risorsa <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Risorsa - - Timestamp <strong>{0}</strong> - Timestamp <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf index 935bdf30f9a..a37003b77e3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf @@ -512,10 +512,10 @@ 詳細 - - Duration <strong>{0}</strong> - 期間 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ リンク - - Resource <strong>{0}</strong> - リソース <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource リソース + + Resource + Resource + + Span スパン @@ -552,10 +552,10 @@ スパン - - Start time <strong>{0}</strong> - 開始時刻 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ ログ エントリ - - Resource <strong>{0}</strong> - リソース <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource リソース - - Timestamp <strong>{0}</strong> - タイムスタンプ <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf index 6e1f067bcdc..c6cfa69bddf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf @@ -512,10 +512,10 @@ 세부 정보 - - Duration <strong>{0}</strong> - 기간 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ 링크 - - Resource <strong>{0}</strong> - 리소스 <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource 리소스 + + Resource + Resource + + Span 범위 @@ -552,10 +552,10 @@ 범위 - - Start time <strong>{0}</strong> - 시작 시간 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ 로그 항목 - - Resource <strong>{0}</strong> - 리소스 <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource 리소스 - - Timestamp <strong>{0}</strong> - 타임스탬프 <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf index 81701dcb329..6077b55b0cc 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf @@ -512,10 +512,10 @@ Szczegóły - - Duration <strong>{0}</strong> - Czas trwania: <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Linki - - Resource <strong>{0}</strong> - Zasób <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Zasób + + Resource + Resource + + Span Zakres @@ -552,10 +552,10 @@ Zakres - - Start time <strong>{0}</strong> - Czas rozpoczęcia: <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Wpis dziennika - - Resource <strong>{0}</strong> - Zasób <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Zasób - - Timestamp <strong>{0}</strong> - Sygnatura czasowa <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf index 49d6196ed6d..e2a26436b44 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf @@ -512,10 +512,10 @@ Detalhes - - Duration <strong>{0}</strong> - Duração <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Links - - Resource <strong>{0}</strong> - Recurso <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Recurso + + Resource + Resource + + Span Intervalo @@ -552,10 +552,10 @@ Intervalo - - Start time <strong>{0}</strong> - Horário de início <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Entrada de log - - Resource <strong>{0}</strong> - Recurso <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Recurso - - Timestamp <strong>{0}</strong> - Carimbo de data <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf index fce8cdd22ce..18f2b8d0ed8 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf @@ -512,10 +512,10 @@ Сведения - - Duration <strong>{0}</strong> - Длительность: <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Ссылки - - Resource <strong>{0}</strong> - Ресурс <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Ресурс + + Resource + Resource + + Span Диапазон @@ -552,10 +552,10 @@ Диапазон - - Start time <strong>{0}</strong> - <strong>Время начала: </strong> {0} - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Запись журнала - - Resource <strong>{0}</strong> - Ресурс <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Ресурс - - Timestamp <strong>{0}</strong> - Метка времени <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf index 6b220adba94..e2f5fd67dc1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf @@ -512,10 +512,10 @@ Ayrıntılar - - Duration <strong>{0}</strong> - Süre <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ Bağlantılar - - Resource <strong>{0}</strong> - Kaynak <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Kaynak + + Resource + Resource + + Span Yayılma @@ -552,10 +552,10 @@ Yayılma - - Start time <strong>{0}</strong> - Başlangıç zamanı <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ Günlük girişi - - Resource <strong>{0}</strong> - Kaynak <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource Kaynak - - Timestamp <strong>{0}</strong> - Zaman damgası <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf index 26e26e9b5b7..38b62ab6f22 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf @@ -512,10 +512,10 @@ 详细信息 - - Duration <strong>{0}</strong> - 持续时间 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ 链接 - - Resource <strong>{0}</strong> - 资源 <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource 资源 + + Resource + Resource + + Span 范围 @@ -552,10 +552,10 @@ 范围 - - Start time <strong>{0}</strong> - 开始时间 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ 日志条目 - - Resource <strong>{0}</strong> - 资源 <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource 资源 - - Timestamp <strong>{0}</strong> - 时间戳 <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf index e9c8854cfda..53404b1815c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf @@ -512,10 +512,10 @@ 詳細資料 - - Duration <strong>{0}</strong> - 期間 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Duration + Duration + Events @@ -527,16 +527,16 @@ 連結 - - Resource <strong>{0}</strong> - 資源 <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource 資源 + + Resource + Resource + + Span 跨度 @@ -552,10 +552,10 @@ 跨度 - - Start time <strong>{0}</strong> - 開始時間 <strong>{0}</strong> - {0} is a duration. This is raw markup, so <strong> and </strong> should not be modified + + Start time + Start time + Cloud @@ -607,20 +607,20 @@ 記錄項目 - - Resource <strong>{0}</strong> - 資源 <strong>{0}</strong> - {0} is the application name. This is raw markup, so <strong> and </strong> should not be modified - Resource 資源 - - Timestamp <strong>{0}</strong> - 時間戳記 <strong>{0}</strong> - {0} is a date. This is raw markup, so <strong> and </strong> should not be modified + + Resource + Resource + + + + Timestamp + Timestamp + Time offset From 6dcf53833b2aa021a3f2b3b03e1c9fafc61f1848 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:01:50 -0700 Subject: [PATCH 03/55] [release/13.3] Ensure compute environment prepare waits for validation (#16583) * Ensure compute environment prepare waits for validation When Foundry is used with another compute environment, the compute environments get confused about who takes ownership of which compute. Make all compute-environment prepare pipeline steps depend on the shared validate-compute-environments step so before-start cannot race environment validation. Add diagnostics coverage for a mixed Foundry hosted-agent and Azure Container Apps app. Cover deployment target lookup returning null for a different compute environment. * Fix tests --------- Co-authored-by: Eric Erhardt --- .../FoundryAgents.AppHost/AppHost.cs | 3 + .../FoundryAgents.AppHost.csproj | 1 + .../AzureContainerAppEnvironmentResource.cs | 2 +- .../AzureAppServiceEnvironmentResource.cs | 2 +- .../AzureKubernetesEnvironmentResource.cs | 1 + .../DockerComposeEnvironmentResource.cs | 4 +- .../HostedAgent/AzureHostedAgentResource.cs | 2 +- .../HostedAgentBuilderExtension.cs | 10 +- .../Project/ProjectResource.cs | 2 +- .../KubernetesEnvironmentResource.cs | 1 + .../ApplicationModel/ResourceExtensions.cs | 13 +- .../DistributedApplicationPipeline.cs | 2 +- .../Pipelines/WellKnownPipelineSteps.cs | 6 + .../AzureDeployerTests.cs | 30 + ..._DoesNotHang_step=diagnostics.verified.txt | 18 +- ...ps_CreatesCorrectDependencies.verified.txt | 696 ++++++++++++++++++ ...nments_Works_step=diagnostics.verified.txt | 30 +- ...ts_CreatesCorrectDependencies.verified.txt | 18 +- ...on_CreatesCorrectDependencies.verified.txt | 18 +- ...TwoPassScanningGeneratedAspire.verified.go | 2 + ...oPassScanningGeneratedAspire.verified.java | 3 + ...TwoPassScanningGeneratedAspire.verified.py | 2 + ...TwoPassScanningGeneratedAspire.verified.rs | 5 + ...TwoPassScanningGeneratedAspire.verified.ts | 3 + .../ResourceExtensionsTests.cs | 19 + 25 files changed, 841 insertions(+), 52 deletions(-) create mode 100644 tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt diff --git a/playground/FoundryAgents/FoundryAgents.AppHost/AppHost.cs b/playground/FoundryAgents/FoundryAgents.AppHost/AppHost.cs index d0e727e7223..04302915a42 100644 --- a/playground/FoundryAgents/FoundryAgents.AppHost/AppHost.cs +++ b/playground/FoundryAgents/FoundryAgents.AppHost/AppHost.cs @@ -8,6 +8,8 @@ var builder = DistributedApplication.CreateBuilder(args); +var aca = builder.AddAzureContainerAppEnvironment("env"); + var foundry = builder.AddFoundry("aif-myfoundry"); var project = foundry.AddProject("proj-myproject") // workaround for https://github.com/microsoft/aspire/issues/15971 @@ -71,6 +73,7 @@ create funny charts or calculations about the topic. builder.AddProject("chat-app") .WithExternalHttpEndpoints() + .WithComputeEnvironment(aca) .WithReference(jokerAgent).WaitFor(jokerAgent) .WithReference(researchAgent).WaitFor(researchAgent); diff --git a/playground/FoundryAgents/FoundryAgents.AppHost/FoundryAgents.AppHost.csproj b/playground/FoundryAgents/FoundryAgents.AppHost/FoundryAgents.AppHost.csproj index 9620d21ef48..8cffc7feadd 100644 --- a/playground/FoundryAgents/FoundryAgents.AppHost/FoundryAgents.AppHost.csproj +++ b/playground/FoundryAgents/FoundryAgents.AppHost/FoundryAgents.AppHost.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs index 53fd954ee10..fece4869a97 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs @@ -50,7 +50,7 @@ public AzureContainerAppEnvironmentResource(string name, Action PrepareDeploymentTargetsAsync(ctx), - DependsOnSteps = [AzureEnvironmentResource.PrepareResourcesStepName], + DependsOnSteps = [AzureEnvironmentResource.PrepareResourcesStepName, WellKnownPipelineSteps.ValidateComputeEnvironments], RequiredBySteps = [WellKnownPipelineSteps.BeforeStart] }; diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs index 0024accb6f3..2e9e39a29a4 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs @@ -51,7 +51,7 @@ public AzureAppServiceEnvironmentResource(string name, Action PrepareDeploymentTargetsAsync(ctx), - DependsOnSteps = [AzureEnvironmentResource.PrepareResourcesStepName], + DependsOnSteps = [AzureEnvironmentResource.PrepareResourcesStepName, WellKnownPipelineSteps.ValidateComputeEnvironments], RequiredBySteps = [WellKnownPipelineSteps.BeforeStart] }; diff --git a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs index e36747ea9b0..2cce7bdb839 100644 --- a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs @@ -46,6 +46,7 @@ public AzureKubernetesEnvironmentResource( Name = $"prepare-aks-{Name}", Description = $"Prepares Azure Kubernetes Service environment {Name}.", Action = ctx => PrepareAksEnvironmentAsync(ctx), + DependsOnSteps = [WellKnownPipelineSteps.ValidateComputeEnvironments], RequiredBySteps = [ WellKnownPipelineSteps.BeforeStart, diff --git a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs index d562ebf49ae..d0054a28dc9 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs @@ -70,6 +70,7 @@ public DockerComposeEnvironmentResource(string name) : base(name) Name = $"prepare-deployment-targets-{Name}", Description = $"Prepares Docker Compose deployment targets for {Name}.", Action = ctx => PrepareDeploymentTargetsAsync(ctx), + DependsOnSteps = [WellKnownPipelineSteps.ValidateComputeEnvironments], RequiredBySteps = [WellKnownPipelineSteps.BeforeStart] }; steps.Add(prepareDeploymentTargetsStep); @@ -118,7 +119,8 @@ public DockerComposeEnvironmentResource(string name) : base(name) { Name = $"prepare-{Name}", Description = $"Prepares the Docker Compose environment {Name} for deployment.", - Action = ctx => PrepareAsync(ctx) + Action = ctx => PrepareAsync(ctx), + DependsOnSteps = [WellKnownPipelineSteps.ValidateComputeEnvironments] }; prepareStep.DependsOn(WellKnownPipelineSteps.Publish); prepareStep.DependsOn(WellKnownPipelineSteps.Build); diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs b/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs index d520765783d..fc0c69476df 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs @@ -15,7 +15,7 @@ namespace Aspire.Hosting.Foundry; /// /// A Microsoft Foundry hosted agent resource. /// -public class AzureHostedAgentResource : Resource, IComputeResource, IResourceWithEnvironment +public class AzureHostedAgentResource : Resource, IResourceWithEnvironment { /// /// Creates a new instance of the class. diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs index 2299b2fa498..8d9f7b1308f 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs @@ -261,6 +261,9 @@ await interactionService.PromptMessageBoxAsync( project = builder.ApplicationBuilder.CreateResourceBuilder(projResource); } } + + builder.WithComputeEnvironment(project); + // Hosted Agent resource name var agentName = $"{resource.Name}-ha"; if (builder.ApplicationBuilder.TryCreateResourceBuilder(agentName, out var rb)) @@ -304,9 +307,12 @@ await interactionService.PromptMessageBoxAsync( { throw new InvalidOperationException($"Unable to create hosted agent for resource '{resource.Name}' because it is not a container, executable, or project resource."); } - - target = resource; + else + { + target = resource; + } } + // Create a separate agent resource to host the deployment var agent = new AzureHostedAgentResource(agentName, target, configure); diff --git a/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs b/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs index 8ed2448b8f5..697b7cafbd8 100644 --- a/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs +++ b/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs @@ -59,7 +59,7 @@ public AzureCognitiveServicesProjectResource([ResourceName] string name, Action< return Task.CompletedTask; }, Resource = this, - DependsOnSteps = [AzureEnvironmentResource.PrepareResourcesStepName], + DependsOnSteps = [AzureEnvironmentResource.PrepareResourcesStepName, WellKnownPipelineSteps.ValidateComputeEnvironments], RequiredBySteps = [WellKnownPipelineSteps.BeforeStart] }; steps.Add(removeDefaultContainerRegistryStep); diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs index a17d583828f..8aa7e3fabc5 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs @@ -200,6 +200,7 @@ public KubernetesEnvironmentResource(string name) : base(name) Name = $"prepare-deployment-targets-{Name}", Description = $"Prepares Kubernetes deployment targets for {Name}.", Action = ctx => PrepareDeploymentTargetsAsync(ctx), + DependsOnSteps = [WellKnownPipelineSteps.ValidateComputeEnvironments], RequiredBySteps = [WellKnownPipelineSteps.BeforeStart] }; diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index dab51d4862f..ba1ee31ee5f 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -999,7 +999,7 @@ internal static bool IsBuildOnlyContainer(this IResource resource) if (resource.TryGetLastAnnotation(out var computeEnvironmentAnnotation)) { // If you have a ComputeEnvironmentAnnotation, it means the resource is bound to a specific compute environment. - // Skip the annotation if it doesn't match the specified computeEnvironmentResource. + // Skip the annotation if it doesn't match the specified targetComputeEnvironment. if (targetComputeEnvironment is not null && targetComputeEnvironment != computeEnvironmentAnnotation.ComputeEnvironment) { return null; @@ -1024,7 +1024,16 @@ internal static bool IsBuildOnlyContainer(this IResource resource) throw new InvalidOperationException($"Resource '{resource.Name}' has multiple compute environments - '{computeEnvironmentNames}'. Please specify a single compute environment using 'WithComputeEnvironment'."); } - return annotations[0]; + var deploymentTargetAnnotation = annotations[0]; + + // If you have a DeploymentTargetAnnotation, it means the resource is bound to a specific compute environment. + // Skip the annotation if it doesn't match the specified targetComputeEnvironment. + if (targetComputeEnvironment is not null && targetComputeEnvironment != deploymentTargetAnnotation.ComputeEnvironment) + { + return null; + } + + return deploymentTargetAnnotation; } return null; } diff --git a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs index 1e2bdad15da..41ea625621a 100644 --- a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs +++ b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs @@ -336,7 +336,7 @@ public DistributedApplicationPipeline() _steps.Add(new PipelineStep { - Name = "validate-compute-environments", + Name = WellKnownPipelineSteps.ValidateComputeEnvironments, Description = "Validates compute resource bindings before startup.", Action = static context => { diff --git a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs index 0df1c31fe9b..dca3b069fcb 100644 --- a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs +++ b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs @@ -74,6 +74,12 @@ public static class WellKnownPipelineSteps [AspireValue("WellKnownPipelineSteps")] public const string Diagnostics = "diagnostics"; + /// + /// The step that validates compute resources are assigned to unambiguous compute environments. + /// + [AspireValue("WellKnownPipelineSteps")] + public const string ValidateComputeEnvironments = "validate-compute-environments"; + /// /// The step that runs before the application starts. /// diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index cb59c6a75c7..98833afb486 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -1271,6 +1271,36 @@ public async Task DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDep await Verify(logs); } + [Fact] + public async Task DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: "diagnostics"); + var mockActivityReporter = new TestPipelineActivityReporter(testOutputHelper); + ConfigureTestServices(builder, activityReporter: mockActivityReporter); + + var foundryProject = builder.AddFoundry("foundry") + .AddProject("foundry-project"); + var acaEnv = builder.AddAzureContainerAppEnvironment("aca-env"); + + builder.AddProject("agent", launchProfileName: null) + .PublishAsHostedAgent(foundryProject); + + builder.AddProject("api", launchProfileName: null) + .WithExternalHttpEndpoints() + .WithComputeEnvironment(acaEnv); + + using var app = builder.Build(); + await app.StartAsync(); + await app.WaitForShutdownAsync(); + + var logs = mockActivityReporter.LoggedMessages + .Where(s => s.StepTitle == "diagnostics") + .Select(s => s.Message) + .ToList(); + + await Verify(logs); + } + [Fact] public async Task DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies() { diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt index 53773bb6d4b..a21d8c91380 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt @@ -1,4 +1,4 @@ -[ +[ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS ===================================== @@ -15,9 +15,9 @@ This shows the order in which steps would execute, respecting all dependencies. Steps with no dependencies run first, followed by steps that depend on them. 1. azure-prepare-resources - 2. prepare-azure-app-service-env - 3. validate-azure-app-service - 4. validate-compute-environments + 2. validate-compute-environments + 3. prepare-azure-app-service-env + 4. validate-azure-app-service 5. before-start 6. process-parameters 7. build-prereq @@ -127,7 +127,7 @@ Step: login-to-acr-env-acr Step: prepare-azure-app-service-env Description: Prepares Azure App Service deployment targets for env. - Dependencies: ✓ azure-prepare-resources + Dependencies: ✓ azure-prepare-resources, ✓ validate-compute-environments Resource: env (AzureAppServiceEnvironmentResource) Step: print-api-summary @@ -378,10 +378,10 @@ If targeting 'login-to-acr-env-acr': [5] login-to-acr-env-acr If targeting 'prepare-azure-app-service-env': - Direct dependencies: azure-prepare-resources - Total steps: 2 + Direct dependencies: azure-prepare-resources, validate-compute-environments + Total steps: 3 Execution order: - [0] azure-prepare-resources + [0] azure-prepare-resources | validate-compute-environments (parallel) [1] prepare-azure-app-service-env If targeting 'print-api-summary': @@ -600,4 +600,4 @@ If targeting 'validate-compute-environments': [0] validate-compute-environments -] +] \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt new file mode 100644 index 00000000000..9964a2d210c --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt @@ -0,0 +1,696 @@ +[ +PIPELINE DEPENDENCY GRAPH DIAGNOSTICS +===================================== + +This diagnostic output shows the complete pipeline dependency graph structure. +Use this to understand step relationships and troubleshoot execution issues. + +Total steps defined: 42 + +Analysis for full pipeline execution (showing all steps and their relationships) + +EXECUTION ORDER +=============== +This shows the order in which steps would execute, respecting all dependencies. +Steps with no dependencies run first, followed by steps that depend on them. + + 1. azure-prepare-resources + 2. validate-compute-environments + 3. prepare-azure-container-apps-aca-env + 4. prepare-foundry-project-foundry-project + 5. validate-azure-container-apps + 6. before-start + 7. process-parameters + 8. build-prereq + 9. check-container-runtime + 10. deploy-prereq + 11. build-agent + 12. build-api + 13. build + 14. validate-azure-login + 15. create-provisioning-context + 16. provision-aca-env-acr + 17. provision-aca-env + 18. login-to-acr-aca-env-acr + 19. provision-foundry-project-acr + 20. login-to-acr-foundry-project-acr + 21. push-prereq + 22. push-api + 23. provision-api-containerapp + 24. provision-foundry + 25. provision-foundry-project + 26. provision-azure-bicep-resources + 27. compute-endpoints-foundry-project + 28. push-agent + 29. deploy-agent-ha + 30. print-api-summary + 31. print-dashboard-url-aca-env + 32. deploy + 33. deploy-api + 34. destroy-prereq + 35. destroy-azure-azure634f9 + 36. destroy + 37. diagnostics + 38. publish-prereq + 39. publish-azure634f9 + 40. publish + 41. publish-manifest + 42. push + +DETAILED STEP ANALYSIS +====================== +Shows each step's dependencies, associated resources, tags, and descriptions. +✓ = dependency exists, ? = dependency missing + +Step: azure-prepare-resources + Description: Prepares the Azure resources. + Dependencies: none + Resource: azure634f9 (AzureEnvironmentResource) + +Step: before-start + Description: Aggregation step for operations that run before the application starts. + Dependencies: ✓ azure-prepare-resources, ✓ prepare-azure-container-apps-aca-env, ✓ prepare-foundry-project-foundry-project, ✓ validate-azure-container-apps, ✓ validate-compute-environments + +Step: build + Description: Aggregation step for all build operations. All build steps should be required by this step. + Dependencies: ✓ build-agent, ✓ build-api + +Step: build-agent + Description: Builds the container image for the agent project. + Dependencies: ✓ build-prereq, ✓ check-container-runtime, ✓ deploy-prereq + Resource: agent (ProjectResource) + Tags: build-compute + +Step: build-api + Description: Builds the container image for the api project. + Dependencies: ✓ build-prereq, ✓ check-container-runtime, ✓ deploy-prereq + Resource: api (ProjectResource) + Tags: build-compute + +Step: build-prereq + Description: Prerequisite step that runs before any build operations. + Dependencies: ✓ process-parameters + +Step: check-container-runtime + Description: Checks whether the container runtime (Docker/Podman) is running. + Dependencies: none + +Step: compute-endpoints-foundry-project + Dependencies: ✓ provision-azure-bicep-resources + Resource: foundry-project (AzureCognitiveServicesProjectResource) + +Step: create-provisioning-context + Description: Creates the Azure provisioning context for infrastructure deployment. + Dependencies: ✓ deploy-prereq, ✓ validate-azure-login + Resource: azure634f9 (AzureEnvironmentResource) + +Step: deploy + Description: Aggregation step for all deploy operations. All deploy steps should be required by this step. + Dependencies: ✓ build-agent, ✓ build-api, ✓ compute-endpoints-foundry-project, ✓ create-provisioning-context, ✓ deploy-agent-ha, ✓ print-api-summary, ✓ print-dashboard-url-aca-env, ✓ provision-azure-bicep-resources, ✓ validate-azure-login + +Step: deploy-agent-ha + Dependencies: ✓ deploy-prereq, ✓ provision-azure-bicep-resources, ✓ push-agent + Resource: agent-ha (AzureHostedAgentResource) + Tags: deploy-compute + +Step: deploy-api + Description: Aggregation step for deploying api to Azure Container Apps. + Dependencies: ✓ print-api-summary + Resource: api-containerapp (AzureContainerAppResource) + Tags: deploy-compute + +Step: deploy-prereq + Description: Prerequisite step that runs before any deploy operations. Initializes deployment environment and manages deployment state. + Dependencies: ✓ process-parameters + +Step: destroy + Description: Aggregation step for all destroy operations. All destroy steps should be required by this step. + Dependencies: ✓ destroy-azure-azure634f9 + +Step: destroy-azure-azure634f9 + Description: Destroys the Azure resource group and all resources for azure634f9. + Dependencies: ✓ destroy-prereq + Resource: azure634f9 (AzureEnvironmentResource) + +Step: destroy-prereq + Description: Prerequisite step that runs before any destroy operations. + Dependencies: none + +Step: diagnostics + Description: Dumps dependency graph information for troubleshooting pipeline execution. + Dependencies: none + +Step: login-to-acr-aca-env-acr + Dependencies: ✓ provision-aca-env-acr + Resource: aca-env-acr (AzureContainerRegistryResource) + Tags: acr-login + +Step: login-to-acr-foundry-project-acr + Dependencies: ✓ provision-foundry-project-acr + Resource: foundry-project-acr (AzureContainerRegistryResource) + Tags: acr-login + +Step: prepare-azure-container-apps-aca-env + Description: Prepares Azure Container Apps deployment targets for aca-env. + Dependencies: ✓ azure-prepare-resources, ✓ validate-compute-environments + Resource: aca-env (AzureContainerAppEnvironmentResource) + +Step: prepare-foundry-project-foundry-project + Description: Prepares Microsoft Foundry project foundry-project for deployment. + Dependencies: ✓ azure-prepare-resources, ✓ validate-compute-environments + Resource: foundry-project (AzureCognitiveServicesProjectResource) + +Step: print-api-summary + Description: Prints the deployment summary and URL for api. + Dependencies: ✓ provision-api-containerapp + Resource: api-containerapp (AzureContainerAppResource) + Tags: print-summary + +Step: print-dashboard-url-aca-env + Description: Prints the deployment summary and dashboard URL for aca-env. + Dependencies: ✓ provision-aca-env, ✓ provision-azure-bicep-resources + Resource: aca-env (AzureContainerAppEnvironmentResource) + Tags: print-summary + +Step: process-parameters + Description: Prompts for parameter values before build, publish, or deployment operations. + Dependencies: none + +Step: provision-aca-env + Description: Provisions the Azure Bicep resource aca-env using Azure infrastructure. + Dependencies: ✓ create-provisioning-context, ✓ provision-aca-env-acr + Resource: aca-env (AzureContainerAppEnvironmentResource) + Tags: provision-infra + +Step: provision-aca-env-acr + Description: Provisions the Azure Bicep resource aca-env-acr using Azure infrastructure. + Dependencies: ✓ create-provisioning-context + Resource: aca-env-acr (AzureContainerRegistryResource) + Tags: provision-infra + +Step: provision-api-containerapp + Description: Provisions the Azure Bicep resource api-containerapp using Azure infrastructure. + Dependencies: ✓ create-provisioning-context, ✓ provision-aca-env, ✓ push-api + Resource: api-containerapp (AzureContainerAppResource) + Tags: provision-infra + +Step: provision-azure-bicep-resources + Description: Aggregation step for all Azure infrastructure provisioning operations. + Dependencies: ✓ create-provisioning-context, ✓ deploy-prereq, ✓ provision-aca-env, ✓ provision-aca-env-acr, ✓ provision-api-containerapp, ✓ provision-foundry, ✓ provision-foundry-project, ✓ provision-foundry-project-acr + Resource: azure634f9 (AzureEnvironmentResource) + Tags: provision-infra + +Step: provision-foundry + Description: Provisions the Azure Bicep resource foundry using Azure infrastructure. + Dependencies: ✓ create-provisioning-context + Resource: foundry (FoundryResource) + Tags: provision-infra + +Step: provision-foundry-project + Description: Provisions the Azure Bicep resource foundry-project using Azure infrastructure. + Dependencies: ✓ create-provisioning-context, ✓ provision-foundry, ✓ provision-foundry-project-acr + Resource: foundry-project (AzureCognitiveServicesProjectResource) + Tags: provision-infra + +Step: provision-foundry-project-acr + Description: Provisions the Azure Bicep resource foundry-project-acr using Azure infrastructure. + Dependencies: ✓ create-provisioning-context + Resource: foundry-project-acr (AzureContainerRegistryResource) + Tags: provision-infra + +Step: publish + Description: Aggregation step for all publish operations. All publish steps should be required by this step. + Dependencies: ✓ publish-azure634f9 + +Step: publish-azure634f9 + Description: Publishes the Azure environment configuration for azure634f9. + Dependencies: ✓ publish-prereq + Resource: azure634f9 (AzureEnvironmentResource) + +Step: publish-manifest + Description: Publishes the Aspire application model as a JSON manifest file. + Dependencies: none + +Step: publish-prereq + Description: Prerequisite step that runs before any publish operations. + Dependencies: ✓ process-parameters + +Step: push + Description: Aggregation step for all push operations. All push steps should be required by this step. + Dependencies: ✓ push-agent, ✓ push-api, ✓ push-prereq + +Step: push-agent + Dependencies: ✓ build-agent, ✓ push-prereq + Resource: agent (ProjectResource) + Tags: push-container-image + +Step: push-api + Dependencies: ✓ build-api, ✓ push-prereq + Resource: api (ProjectResource) + Tags: push-container-image + +Step: push-prereq + Description: Prerequisite step that runs before any push operations. + Dependencies: ✓ login-to-acr-aca-env-acr, ✓ login-to-acr-foundry-project-acr + +Step: validate-azure-container-apps + Dependencies: none + +Step: validate-azure-login + Description: Validates Azure CLI authentication before deployment. + Dependencies: ✓ deploy-prereq + Resource: azure634f9 (AzureEnvironmentResource) + +Step: validate-compute-environments + Description: Validates compute resource bindings before startup. + Dependencies: none + +POTENTIAL ISSUES: +Identifies problems in the pipeline configuration that could prevent execution. +───────────────── +INFO: Orphaned steps (no dependencies, not required by others): + - diagnostics + - publish-manifest + +EXECUTION SIMULATION ("What If" Analysis): +Shows what steps would run for each possible target step and in what order. +Steps at the same level can run concurrently. +───────────────────────────────────────────────────────────────────────────── +If targeting 'azure-prepare-resources': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] azure-prepare-resources + +If targeting 'before-start': + Direct dependencies: azure-prepare-resources, prepare-azure-container-apps-aca-env, prepare-foundry-project-foundry-project, validate-azure-container-apps, validate-compute-environments + Total steps: 6 + Execution order: + [0] azure-prepare-resources | validate-azure-container-apps | validate-compute-environments (parallel) + [1] prepare-azure-container-apps-aca-env | prepare-foundry-project-foundry-project (parallel) + [2] before-start + +If targeting 'build': + Direct dependencies: build-agent, build-api + Total steps: 7 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-agent | build-api (parallel) + [3] build + +If targeting 'build-agent': + Direct dependencies: build-prereq, check-container-runtime, deploy-prereq + Total steps: 5 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-agent + +If targeting 'build-api': + Direct dependencies: build-prereq, check-container-runtime, deploy-prereq + Total steps: 5 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api + +If targeting 'build-prereq': + Direct dependencies: process-parameters + Total steps: 2 + Execution order: + [0] process-parameters + [1] build-prereq + +If targeting 'check-container-runtime': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] check-container-runtime + +If targeting 'compute-endpoints-foundry-project': + Direct dependencies: provision-azure-bicep-resources + Total steps: 19 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env | provision-foundry-project (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-containerapp + [9] provision-azure-bicep-resources + [10] compute-endpoints-foundry-project + +If targeting 'create-provisioning-context': + Direct dependencies: deploy-prereq, validate-azure-login + Total steps: 4 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + +If targeting 'deploy': + Direct dependencies: build-agent, build-api, compute-endpoints-foundry-project, create-provisioning-context, deploy-agent-ha, print-api-summary, print-dashboard-url-aca-env, provision-azure-bicep-resources, validate-azure-login + Total steps: 25 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-agent | build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env | provision-foundry-project (parallel) + [6] push-prereq + [7] push-agent | push-api (parallel) + [8] provision-api-containerapp + [9] print-api-summary | provision-azure-bicep-resources (parallel) + [10] compute-endpoints-foundry-project | deploy-agent-ha | print-dashboard-url-aca-env (parallel) + [11] deploy + +If targeting 'deploy-agent-ha': + Direct dependencies: deploy-prereq, provision-azure-bicep-resources, push-agent + Total steps: 21 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-agent | build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env | provision-foundry-project (parallel) + [6] push-prereq + [7] push-agent | push-api (parallel) + [8] provision-api-containerapp + [9] provision-azure-bicep-resources + [10] deploy-agent-ha + +If targeting 'deploy-api': + Direct dependencies: print-api-summary + Total steps: 17 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-containerapp + [9] print-api-summary + [10] deploy-api + +If targeting 'deploy-prereq': + Direct dependencies: process-parameters + Total steps: 2 + Execution order: + [0] process-parameters + [1] deploy-prereq + +If targeting 'destroy': + Direct dependencies: destroy-azure-azure634f9 + Total steps: 3 + Execution order: + [0] destroy-prereq + [1] destroy-azure-azure634f9 + [2] destroy + +If targeting 'destroy-azure-azure634f9': + Direct dependencies: destroy-prereq + Total steps: 2 + Execution order: + [0] destroy-prereq + [1] destroy-azure-azure634f9 + +If targeting 'destroy-prereq': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] destroy-prereq + +If targeting 'diagnostics': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] diagnostics + +If targeting 'login-to-acr-aca-env-acr': + Direct dependencies: provision-aca-env-acr + Total steps: 6 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-aca-env-acr + [5] login-to-acr-aca-env-acr + +If targeting 'login-to-acr-foundry-project-acr': + Direct dependencies: provision-foundry-project-acr + Total steps: 6 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-foundry-project-acr + [5] login-to-acr-foundry-project-acr + +If targeting 'prepare-azure-container-apps-aca-env': + Direct dependencies: azure-prepare-resources, validate-compute-environments + Total steps: 3 + Execution order: + [0] azure-prepare-resources | validate-compute-environments (parallel) + [1] prepare-azure-container-apps-aca-env + +If targeting 'prepare-foundry-project-foundry-project': + Direct dependencies: azure-prepare-resources, validate-compute-environments + Total steps: 3 + Execution order: + [0] azure-prepare-resources | validate-compute-environments (parallel) + [1] prepare-foundry-project-foundry-project + +If targeting 'print-api-summary': + Direct dependencies: provision-api-containerapp + Total steps: 16 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-containerapp + [9] print-api-summary + +If targeting 'print-dashboard-url-aca-env': + Direct dependencies: provision-aca-env, provision-azure-bicep-resources + Total steps: 19 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env | provision-foundry-project (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-containerapp + [9] provision-azure-bicep-resources + [10] print-dashboard-url-aca-env + +If targeting 'process-parameters': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] process-parameters + +If targeting 'provision-aca-env': + Direct dependencies: create-provisioning-context, provision-aca-env-acr + Total steps: 6 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-aca-env-acr + [5] provision-aca-env + +If targeting 'provision-aca-env-acr': + Direct dependencies: create-provisioning-context + Total steps: 5 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-aca-env-acr + +If targeting 'provision-api-containerapp': + Direct dependencies: create-provisioning-context, provision-aca-env, push-api + Total steps: 15 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-containerapp + +If targeting 'provision-azure-bicep-resources': + Direct dependencies: create-provisioning-context, deploy-prereq, provision-aca-env, provision-aca-env-acr, provision-api-containerapp, provision-foundry, provision-foundry-project, provision-foundry-project-acr + Total steps: 18 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr | provision-aca-env | provision-foundry-project (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-containerapp + [9] provision-azure-bicep-resources + +If targeting 'provision-foundry': + Direct dependencies: create-provisioning-context + Total steps: 5 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-foundry + +If targeting 'provision-foundry-project': + Direct dependencies: create-provisioning-context, provision-foundry, provision-foundry-project-acr + Total steps: 7 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-foundry | provision-foundry-project-acr (parallel) + [5] provision-foundry-project + +If targeting 'provision-foundry-project-acr': + Direct dependencies: create-provisioning-context + Total steps: 5 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-foundry-project-acr + +If targeting 'publish': + Direct dependencies: publish-azure634f9 + Total steps: 4 + Execution order: + [0] process-parameters + [1] publish-prereq + [2] publish-azure634f9 + [3] publish + +If targeting 'publish-azure634f9': + Direct dependencies: publish-prereq + Total steps: 3 + Execution order: + [0] process-parameters + [1] publish-prereq + [2] publish-azure634f9 + +If targeting 'publish-manifest': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] publish-manifest + +If targeting 'publish-prereq': + Direct dependencies: process-parameters + Total steps: 2 + Execution order: + [0] process-parameters + [1] publish-prereq + +If targeting 'push': + Direct dependencies: push-agent, push-api, push-prereq + Total steps: 16 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-agent | build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr (parallel) + [6] push-prereq + [7] push-agent | push-api (parallel) + [8] push + +If targeting 'push-agent': + Direct dependencies: build-agent, push-prereq + Total steps: 13 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-agent | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr (parallel) + [6] push-prereq + [7] push-agent + +If targeting 'push-api': + Direct dependencies: build-api, push-prereq + Total steps: 13 + Execution order: + [0] check-container-runtime | process-parameters (parallel) + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr (parallel) + [6] push-prereq + [7] push-api + +If targeting 'push-prereq': + Direct dependencies: login-to-acr-aca-env-acr, login-to-acr-foundry-project-acr + Total steps: 9 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-aca-env-acr | provision-foundry-project-acr (parallel) + [5] login-to-acr-aca-env-acr | login-to-acr-foundry-project-acr (parallel) + [6] push-prereq + +If targeting 'validate-azure-container-apps': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] validate-azure-container-apps + +If targeting 'validate-azure-login': + Direct dependencies: deploy-prereq + Total steps: 3 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + +If targeting 'validate-compute-environments': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] validate-compute-environments + + +] \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt index c4d840f8c50..531661150c9 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt @@ -1,4 +1,4 @@ -[ +[ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS ===================================== @@ -15,11 +15,11 @@ This shows the order in which steps would execute, respecting all dependencies. Steps with no dependencies run first, followed by steps that depend on them. 1. azure-prepare-resources - 2. prepare-azure-app-service-aas-env - 3. prepare-azure-container-apps-aca-env - 4. validate-azure-app-service - 5. validate-azure-container-apps - 6. validate-compute-environments + 2. validate-compute-environments + 3. prepare-azure-app-service-aas-env + 4. prepare-azure-container-apps-aca-env + 5. validate-azure-app-service + 6. validate-azure-container-apps 7. before-start 8. process-parameters 9. build-prereq @@ -161,12 +161,12 @@ Step: login-to-acr-aca-env-acr Step: prepare-azure-app-service-aas-env Description: Prepares Azure App Service deployment targets for aas-env. - Dependencies: ✓ azure-prepare-resources + Dependencies: ✓ azure-prepare-resources, ✓ validate-compute-environments Resource: aas-env (AzureAppServiceEnvironmentResource) Step: prepare-azure-container-apps-aca-env Description: Prepares Azure Container Apps deployment targets for aca-env. - Dependencies: ✓ azure-prepare-resources + Dependencies: ✓ azure-prepare-resources, ✓ validate-compute-environments Resource: aca-env (AzureContainerAppEnvironmentResource) Step: print-api-service-summary @@ -504,17 +504,17 @@ If targeting 'login-to-acr-aca-env-acr': [5] login-to-acr-aca-env-acr If targeting 'prepare-azure-app-service-aas-env': - Direct dependencies: azure-prepare-resources - Total steps: 2 + Direct dependencies: azure-prepare-resources, validate-compute-environments + Total steps: 3 Execution order: - [0] azure-prepare-resources + [0] azure-prepare-resources | validate-compute-environments (parallel) [1] prepare-azure-app-service-aas-env If targeting 'prepare-azure-container-apps-aca-env': - Direct dependencies: azure-prepare-resources - Total steps: 2 + Direct dependencies: azure-prepare-resources, validate-compute-environments + Total steps: 3 Execution order: - [0] azure-prepare-resources + [0] azure-prepare-resources | validate-compute-environments (parallel) [1] prepare-azure-container-apps-aca-env If targeting 'print-api-service-summary': @@ -822,4 +822,4 @@ If targeting 'validate-compute-environments': [0] validate-compute-environments -] +] \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt index 1350cc2aaf6..2423ce98bca 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt @@ -1,4 +1,4 @@ -[ +[ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS ===================================== @@ -15,9 +15,9 @@ This shows the order in which steps would execute, respecting all dependencies. Steps with no dependencies run first, followed by steps that depend on them. 1. azure-prepare-resources - 2. prepare-azure-container-apps-env - 3. validate-azure-container-apps - 4. validate-compute-environments + 2. validate-compute-environments + 3. prepare-azure-container-apps-env + 4. validate-azure-container-apps 5. before-start 6. process-parameters 7. build-prereq @@ -139,7 +139,7 @@ Step: login-to-acr-env-acr Step: prepare-azure-container-apps-env Description: Prepares Azure Container Apps deployment targets for env. - Dependencies: ✓ azure-prepare-resources + Dependencies: ✓ azure-prepare-resources, ✓ validate-compute-environments Resource: env (AzureContainerAppEnvironmentResource) Step: print-api-summary @@ -465,10 +465,10 @@ If targeting 'login-to-acr-env-acr': [5] login-to-acr-env-acr If targeting 'prepare-azure-container-apps-env': - Direct dependencies: azure-prepare-resources - Total steps: 2 + Direct dependencies: azure-prepare-resources, validate-compute-environments + Total steps: 3 Execution order: - [0] azure-prepare-resources + [0] azure-prepare-resources | validate-compute-environments (parallel) [1] prepare-azure-container-apps-env If targeting 'print-api-summary': @@ -836,4 +836,4 @@ If targeting 'validate-compute-environments': [0] validate-compute-environments -] +] \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt index 9a3c1a42fa4..c0bcc1c442b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt @@ -1,4 +1,4 @@ -[ +[ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS ===================================== @@ -15,9 +15,9 @@ This shows the order in which steps would execute, respecting all dependencies. Steps with no dependencies run first, followed by steps that depend on them. 1. azure-prepare-resources - 2. prepare-azure-app-service-env - 3. validate-azure-app-service - 4. validate-compute-environments + 2. validate-compute-environments + 3. prepare-azure-app-service-env + 4. validate-azure-app-service 5. before-start 6. process-parameters 7. build-prereq @@ -134,7 +134,7 @@ Step: login-to-acr-env-acr Step: prepare-azure-app-service-env Description: Prepares Azure App Service deployment targets for env. - Dependencies: ✓ azure-prepare-resources + Dependencies: ✓ azure-prepare-resources, ✓ validate-compute-environments Resource: env (AzureAppServiceEnvironmentResource) Step: print-api-summary @@ -427,10 +427,10 @@ If targeting 'login-to-acr-env-acr': [5] login-to-acr-env-acr If targeting 'prepare-azure-app-service-env': - Direct dependencies: azure-prepare-resources - Total steps: 2 + Direct dependencies: azure-prepare-resources, validate-compute-environments + Total steps: 3 Execution order: - [0] azure-prepare-resources + [0] azure-prepare-resources | validate-compute-environments (parallel) [1] prepare-azure-app-service-env If targeting 'print-api-summary': @@ -724,4 +724,4 @@ If targeting 'validate-compute-environments': [0] validate-compute-environments -] +] \ No newline at end of file diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index df102d0f896..96d373bcc0f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -574,6 +574,7 @@ var WellKnownPipelineSteps = struct { PublishPrereq string Push string PushPrereq string + ValidateComputeEnvironments string }{ Build: "build", BuildPrereq: "build-prereq", @@ -587,6 +588,7 @@ var WellKnownPipelineSteps = struct { PublishPrereq: "publish-prereq", Push: "push", PushPrereq: "push-prereq", + ValidateComputeEnvironments: "validate-compute-environments", } var WellKnownPipelineTags = struct { diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 3efb0f95a8f..b27d5752d92 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -22438,6 +22438,9 @@ private WellKnownPipelineSteps() { } /** The prerequisite step that runs before any push operations. */ public static final String PushPrereq = "push-prereq"; + /** The step that validates compute resources are assigned to unambiguous compute environments. */ + public static final String ValidateComputeEnvironments = "validate-compute-environments"; + } // ===== WellKnownPipelineTags.java ===== diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 989c53c47da..e0f5c405739 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1864,6 +1864,8 @@ class TestNestedDto(typing.TypedDict, total=False): WellKnownPipelineSteps.Push = "push" # The prerequisite step that runs before any push operations. WellKnownPipelineSteps.PushPrereq = "push-prereq" +# The step that validates compute resources are assigned to unambiguous compute environments. +WellKnownPipelineSteps.ValidateComputeEnvironments = "validate-compute-environments" WellKnownPipelineTags = types.SimpleNamespace() # Tag for steps that build compute resources. diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 579ab8666d6..e1f5035b52f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1020,6 +1020,11 @@ pub mod well_known_pipeline_steps { serde_json::from_value::(serde_json::json!("push-prereq")) .expect("generated exported value should deserialize") } + /// The step that validates compute resources are assigned to unambiguous compute environments. + pub fn validate_compute_environments() -> String { + serde_json::from_value::(serde_json::json!("validate-compute-environments")) + .expect("generated exported value should deserialize") + } } pub mod well_known_pipeline_tags { diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 3b02b64a29a..fbfb93d7f97 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -647,6 +647,9 @@ export namespace WellKnownPipelineSteps { /** The prerequisite step that runs before any push operations. */ export const PushPrereq = "push-prereq"; + /** The step that validates compute resources are assigned to unambiguous compute environments. */ + export const ValidateComputeEnvironments = "validate-compute-environments"; + } export namespace WellKnownPipelineTags { diff --git a/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs b/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs index e30ae623550..87b4793d550 100644 --- a/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs @@ -144,6 +144,25 @@ public void TryGetAnnotationsIncludingAncestorsOfTypeCombinesAnnotationsFromPare Assert.Equal(3, annotations.Count()); } + [Fact] + public void GetDeploymentTargetAnnotation_ReturnsNullForDifferentTargetComputeEnvironment() + { + using var builder = TestDistributedApplicationBuilder.Create(); + var requestedEnvironment = builder.AddResource(new ComputeEnvironmentResource("env1")); + var annotationEnvironment = builder.AddResource(new ComputeEnvironmentResource("env2")); + var deploymentTarget = builder.AddResource(new ParentResource("target")); + + var resource = builder.AddResource(new ParentResource("resource")) + .WithAnnotation(new DeploymentTargetAnnotation(deploymentTarget.Resource) + { + ComputeEnvironment = annotationEnvironment.Resource + }); + + var annotation = resource.Resource.GetDeploymentTargetAnnotation(requestedEnvironment.Resource); + + Assert.Null(annotation); + } + [Fact] public void TryGetContainerImageNameReturnsCorrectFormatWhenShaSupplied() { From bb4d2cd3ee05e9261a89b8698a350bf50e502a62 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:02:45 -0700 Subject: [PATCH 04/55] [release/13.3] Normalize App Service Application Insights Bicep identifiers (#16564) * Initial plan * Normalize app service app insights bicep identifiers Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/e848458a-5173-4241-b442-80d01de3d5c2 Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> * Preserve app insights bicep suffix separators Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/e848458a-5173-4241-b442-80d01de3d5c2 Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> * Address PR feedback --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> Co-authored-by: Eric Erhardt --- .../AzureAppServiceEnvironmentExtensions.cs | 12 +- .../AzureAppServiceTests.cs | 20 ++ ...sNormalizesBicepIdentifiers.verified.bicep | 171 ++++++++++++++++++ 3 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsNormalizesBicepIdentifiers.verified.bicep diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs index 89a40883cf6..86af48fbdca 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs @@ -87,8 +87,8 @@ public static IResourceBuilder AddAzureAppSe var resource = new AzureAppServiceEnvironmentResource(name, static infra => { - var prefix = infra.AspireResource.Name; var resource = (AzureAppServiceEnvironmentResource)infra.AspireResource; + var prefix = Infrastructure.NormalizeBicepIdentifier(resource.Name); // This tells azd to avoid creating infrastructure var userPrincipalId = new ProvisioningParameter(AzureBicepResource.KnownParameters.UserPrincipalId, typeof(string)) { Value = new BicepValue(string.Empty) }; @@ -101,7 +101,7 @@ public static IResourceBuilder AddAzureAppSe infra.Add(tags); - var identity = new UserAssignedIdentity(Infrastructure.NormalizeBicepIdentifier($"{prefix}-mi")) + var identity = new UserAssignedIdentity($"{prefix}_mi") { Tags = tags }; @@ -133,7 +133,7 @@ public static IResourceBuilder AddAzureAppSe pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, identity.Id, pullRa.RoleDefinitionId); infra.Add(pullRa); - var plan = new AppServicePlan(Infrastructure.NormalizeBicepIdentifier($"{prefix}-asplan")) + var plan = new AppServicePlan($"{prefix}_asplan") { Sku = new AppServiceSkuDescription { @@ -191,7 +191,7 @@ public static IResourceBuilder AddAzureAppSe infra.Add(new ProvisioningOutput("AZURE_APP_SERVICE_DASHBOARD_URI", typeof(string)) { - Value = BicepFunction.Interpolate($"https://{AzureAppServiceEnvironmentUtility.GetDashboardHostName(prefix)}.azurewebsites.net") + Value = BicepFunction.Interpolate($"https://{AzureAppServiceEnvironmentUtility.GetDashboardHostName(resource.Name)}.azurewebsites.net") }); } @@ -206,7 +206,7 @@ public static IResourceBuilder AddAzureAppSe else { // Create Log Analytics workspace - var logAnalyticsWorkspace = new OperationalInsightsWorkspace(prefix + "_law") + var logAnalyticsWorkspace = new OperationalInsightsWorkspace($"{prefix}_law") { Sku = new OperationalInsightsWorkspaceSku() { @@ -217,7 +217,7 @@ public static IResourceBuilder AddAzureAppSe infra.Add(logAnalyticsWorkspace); // Create Application Insights resource linked to the Log Analytics workspace - applicationInsights = new ApplicationInsightsComponent(prefix + "_ai") + applicationInsights = new ApplicationInsightsComponent($"{prefix}_ai") { ApplicationType = ApplicationInsightsApplicationType.Web, Kind = "web", diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs index defebb44e39..781cc678cc5 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs @@ -846,6 +846,26 @@ await Verify(manifest.ToString(), "json") .AppendContentAsFile(bicep, "bicep"); } + [Fact] + public async Task AddAppServiceWithApplicationInsightsNormalizesBicepIdentifiers() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddAzureAppServiceEnvironment("env-1").WithAzureApplicationInsights(); + + using var app = builder.Build(); + + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + + var environment = Assert.Single(model.Resources.OfType()); + + var (_, bicep) = await GetManifestWithBicep(environment); + + await Verify(bicep, "bicep"); + } + [Fact] public async Task AddAppServiceWithApplicationInsightsLocationParam() { diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsNormalizesBicepIdentifiers.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsNormalizesBicepIdentifiers.verified.bicep new file mode 100644 index 00000000000..08a87488e0a --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsNormalizesBicepIdentifiers.verified.bicep @@ -0,0 +1,171 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param userPrincipalId string = '' + +param tags object = { } + +param env_1_acr_outputs_name string + +resource env_1_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { + name: take('env_1_mi-${uniqueString(resourceGroup().id)}', 128) + location: location + tags: tags +} + +resource env_1_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_1_acr_outputs_name +} + +resource env_1_acr_env_1_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(env_1_acr.id, env_1_mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) + properties: { + principalId: env_1_mi.properties.principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + principalType: 'ServicePrincipal' + } + scope: env_1_acr +} + +resource env_1_asplan 'Microsoft.Web/serverfarms@2025-03-01' = { + name: take('env1asplan-${uniqueString(resourceGroup().id)}', 60) + location: location + properties: { + perSiteScaling: true + reserved: true + } + kind: 'Linux' + sku: { + name: 'P0V3' + tier: 'Premium' + } +} + +resource env_1_contributor_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { + name: take('env_1_contributor_mi-${uniqueString(resourceGroup().id)}', 128) + location: location +} + +resource env_1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, env_1_contributor_mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')) + properties: { + principalId: env_1_contributor_mi.properties.principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + principalType: 'ServicePrincipal' + } +} + +resource dashboard 'Microsoft.Web/sites@2025-03-01' = { + name: take('${toLower('env-1')}-${toLower('aspiredashboard')}-${uniqueString(resourceGroup().id)}', 60) + location: location + properties: { + serverFarmId: env_1_asplan.id + siteConfig: { + numberOfWorkers: 1 + linuxFxVersion: 'ASPIREDASHBOARD|1.0' + acrUseManagedIdentityCreds: true + acrUserManagedIdentityID: env_1_mi.properties.clientId + appSettings: [ + { + name: 'DASHBOARD__FRONTEND__AUTHMODE' + value: 'Unsecured' + } + { + name: 'DASHBOARD__OTLP__AUTHMODE' + value: 'Unsecured' + } + { + name: 'DASHBOARD__OTLP__SUPPRESSUNSECUREDTELEMETRYMESSAGE' + value: 'true' + } + { + name: 'DASHBOARD__RESOURCESERVICECLIENT__AUTHMODE' + value: 'Unsecured' + } + { + name: 'DASHBOARD__UI__DISABLEIMPORT' + value: 'true' + } + { + name: 'WEBSITES_PORT' + value: '5000' + } + { + name: 'HTTP20_ONLY_PORT' + value: '4317' + } + { + name: 'WEBSITE_START_SCM_WITH_PRELOAD' + value: 'true' + } + { + name: 'AZURE_CLIENT_ID' + value: env_1_contributor_mi.properties.clientId + } + { + name: 'ALLOWED_MANAGED_IDENTITIES' + value: env_1_mi.properties.clientId + } + { + name: 'ASPIRE_ENVIRONMENT_NAME' + value: 'env-1' + } + ] + alwaysOn: true + http20Enabled: true + http20ProxyFlag: 1 + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${env_1_contributor_mi.id}': { } + } + } + kind: 'app,linux,aspiredashboard' +} + +resource env_1_law 'Microsoft.OperationalInsights/workspaces@2025-02-01' = { + name: take('env1law-${uniqueString(resourceGroup().id)}', 63) + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } +} + +resource env_1_ai 'Microsoft.Insights/components@2020-02-02' = { + name: take('env_1_ai-${uniqueString(resourceGroup().id)}', 260) + kind: 'web' + location: location + properties: { + Application_Type: 'web' + IngestionMode: 'LogAnalytics' + WorkspaceResourceId: env_1_law.id + } +} + +output name string = env_1_asplan.name + +output planId string = env_1_asplan.id + +output webSiteSuffix string = uniqueString(resourceGroup().id) + +output AZURE_CONTAINER_REGISTRY_NAME string = env_1_acr.name + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_1_acr.properties.loginServer + +output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = env_1_mi.id + +output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID string = env_1_mi.properties.clientId + +output AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID string = env_1_contributor_mi.id + +output AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID string = env_1_contributor_mi.properties.principalId + +output AZURE_APP_SERVICE_DASHBOARD_URI string = 'https://${take('${toLower('env-1')}-${toLower('aspiredashboard')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net' + +output AZURE_APPLICATION_INSIGHTS_INSTRUMENTATIONKEY string = env_1_ai.properties.InstrumentationKey + +output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = env_1_ai.properties.ConnectionString \ No newline at end of file From 7a8d66e73bf28d3a4f37693dae510a9534f8d82a Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:04:19 -0700 Subject: [PATCH 05/55] [release/13.3] Fix TypeScript AppHost package manager detection (#16598) * Fix TypeScript package manager detection Treat package-lock.json as an npm marker and limit parent directory package manager probing to the AppHost directory's direct parent. Log the marker used to select the TypeScript AppHost package manager. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix TypeScript AppHost path comparisons Use OS-appropriate path comparison when deciding whether to skip root and home parent directories for package manager detection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve same-directory yarn marker precedence Keep yarn markers ahead of package-lock.json within the same candidate directory while still allowing a local npm lockfile to beat parent-directory yarn markers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove yarn directory package manager hint Only use file-based yarn markers for TypeScript AppHost package manager detection and update the resolver test accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address TypeScript toolchain review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Sebastien Ros Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Projects/GuestAppHostProject.cs | 2 +- .../TypeScriptAppHostToolchainResolver.cs | 110 +++++++++++----- .../Scaffolding/ScaffoldingService.cs | 6 +- .../TypeScriptAppHostToolingCheck.cs | 2 +- .../Utils/MissingJavaScriptToolWarning.cs | 2 +- ...TypeScriptAppHostToolchainResolverTests.cs | 119 +++++++++++++++--- 6 files changed, 190 insertions(+), 51 deletions(-) diff --git a/src/Aspire.Cli/Projects/GuestAppHostProject.cs b/src/Aspire.Cli/Projects/GuestAppHostProject.cs index 5d250af22c6..ee96d296e7d 100644 --- a/src/Aspire.Cli/Projects/GuestAppHostProject.cs +++ b/src/Aspire.Cli/Projects/GuestAppHostProject.cs @@ -1385,7 +1385,7 @@ private async Task EnsureRuntimeCreatedAsync( var runtimeSpec = await rpcClient.GetRuntimeSpecAsync(_resolvedLanguage.LanguageId, cancellationToken); if (TypeScriptAppHostToolchainResolver.IsTypeScriptLanguage(_resolvedLanguage)) { - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory, _logger); runtimeSpec = TypeScriptAppHostToolchainResolver.ApplyToRuntimeSpec(runtimeSpec, toolchain); } diff --git a/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs b/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs index 5beace2fdd8..778d26a44dc 100644 --- a/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs +++ b/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs @@ -6,6 +6,7 @@ using System.Text.Json.Nodes; using Aspire.Cli.Utils; using Aspire.TypeSystem; +using Microsoft.Extensions.Logging; namespace Aspire.Cli.Projects; @@ -19,14 +20,12 @@ internal enum TypeScriptAppHostToolchain internal static class TypeScriptAppHostToolchainResolver { - internal const int MaxParentSearchDepth = 8; - private const string PackageJsonFileName = "package.json"; private const string BunLockFileName = "bun.lock"; private const string BunBinaryLockFileName = "bun.lockb"; private const string YarnLockFileName = "yarn.lock"; private const string YarnConfigFileName = ".yarnrc.yml"; - private const string YarnDirectoryName = ".yarn"; + private const string PackageLockFileName = "package-lock.json"; private const string PnpmLockFileName = "pnpm-lock.yaml"; public static bool IsTypeScriptLanguage(LanguageInfo? language) @@ -36,35 +35,58 @@ public static bool IsTypeScriptLanguage(LanguageInfo? language) language.LanguageId.Value.Equals(KnownLanguageId.TypeScriptAlias, StringComparison.OrdinalIgnoreCase)); } - public static TypeScriptAppHostToolchain Resolve(DirectoryInfo appHostDirectory) + public static TypeScriptAppHostToolchain Resolve(DirectoryInfo appHostDirectory, ILogger? logger) + { + var resolution = ResolveWithReason(appHostDirectory); + logger?.LogDebug( + "Selected TypeScript AppHost package manager '{PackageManager}' because {Reason}.", + GetCommandName(resolution.Toolchain), + resolution.Reason); + + return resolution.Toolchain; + } + + internal static TypeScriptAppHostToolchainResolution ResolveWithReason(DirectoryInfo appHostDirectory) { foreach (var candidateDirectory in EnumerateCandidateDirectories(appHostDirectory)) { - if (TryGetToolchainFromPackageJson(candidateDirectory, out var configuredToolchain)) + if (TryGetToolchainFromPackageJson(candidateDirectory, out var configuredToolchain, out var reason)) + { + return new(configuredToolchain, reason); + } + + if (File.Exists(Path.Combine(candidateDirectory.FullName, BunLockFileName))) { - return configuredToolchain; + return CreateLockFileResolution(TypeScriptAppHostToolchain.Bun, BunLockFileName, candidateDirectory); } - if (File.Exists(Path.Combine(candidateDirectory.FullName, BunLockFileName)) || - File.Exists(Path.Combine(candidateDirectory.FullName, BunBinaryLockFileName))) + if (File.Exists(Path.Combine(candidateDirectory.FullName, BunBinaryLockFileName))) { - return TypeScriptAppHostToolchain.Bun; + return CreateLockFileResolution(TypeScriptAppHostToolchain.Bun, BunBinaryLockFileName, candidateDirectory); } if (File.Exists(Path.Combine(candidateDirectory.FullName, PnpmLockFileName))) { - return TypeScriptAppHostToolchain.Pnpm; + return CreateLockFileResolution(TypeScriptAppHostToolchain.Pnpm, PnpmLockFileName, candidateDirectory); } - if (File.Exists(Path.Combine(candidateDirectory.FullName, YarnLockFileName)) || - File.Exists(Path.Combine(candidateDirectory.FullName, YarnConfigFileName)) || - Directory.Exists(Path.Combine(candidateDirectory.FullName, YarnDirectoryName))) + if (File.Exists(Path.Combine(candidateDirectory.FullName, YarnLockFileName))) { - return TypeScriptAppHostToolchain.Yarn; + return CreateLockFileResolution(TypeScriptAppHostToolchain.Yarn, YarnLockFileName, candidateDirectory); + } + + if (File.Exists(Path.Combine(candidateDirectory.FullName, YarnConfigFileName))) + { + return CreateLockFileResolution(TypeScriptAppHostToolchain.Yarn, YarnConfigFileName, candidateDirectory); + } + + if (File.Exists(Path.Combine(candidateDirectory.FullName, PackageLockFileName))) + { + return CreateLockFileResolution(TypeScriptAppHostToolchain.Npm, PackageLockFileName, candidateDirectory); } } - return TypeScriptAppHostToolchain.Npm; + return new(TypeScriptAppHostToolchain.Npm, $"no package manager marker found in {appHostDirectory.FullName} or an eligible parent directory"); } public static string[] GetRequiredCommands(TypeScriptAppHostToolchain toolchain) @@ -219,12 +241,15 @@ private static string GetTsConfigFileName(RuntimeSpec runtimeSpec) return "tsconfig.apphost.json"; } - private static bool TryGetToolchainFromPackageJson(DirectoryInfo appHostDirectory, out TypeScriptAppHostToolchain toolchain) + private static bool TryGetToolchainFromPackageJson(DirectoryInfo appHostDirectory, out TypeScriptAppHostToolchain toolchain, out string reason) { + toolchain = default; + reason = string.Empty; + var packageJsonPath = Path.Combine(appHostDirectory.FullName, PackageJsonFileName); if (!File.Exists(packageJsonPath)) { - return SetUnknownToolchain(out toolchain); + return false; } try @@ -234,17 +259,23 @@ private static bool TryGetToolchainFromPackageJson(DirectoryInfo appHostDirector !packageManagerValue.TryGetValue(out var packageManager) || string.IsNullOrWhiteSpace(packageManager)) { - return SetUnknownToolchain(out toolchain); + return false; } var packageManagerName = packageManager.Split('@', 2)[0]; - return TryParseToolchain(packageManagerName, out toolchain); + if (TryParseToolchain(packageManagerName, out toolchain)) + { + reason = $"packageManager '{packageManager}' found in {packageJsonPath}"; + return true; + } + + return false; } catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException or SecurityException or NotSupportedException) { - return SetUnknownToolchain(out toolchain); + return false; } } @@ -265,19 +296,40 @@ private static bool TryParseToolchain(string packageManagerName, out TypeScriptA private static IEnumerable EnumerateCandidateDirectories(DirectoryInfo appHostDirectory) { - // Allow nested AppHosts to pick up workspace-level lockfiles/packageManager settings - // without accidentally walking all the way to an unrelated parent directory. - var currentDirectory = appHostDirectory; - for (var depth = 0; currentDirectory is not null && depth <= MaxParentSearchDepth; depth++) + yield return appHostDirectory; + + // Only use the immediate parent as a fallback so a project folder can provide + // workspace-level hints without inheriting unrelated markers from higher directories. + var parentDirectory = appHostDirectory.Parent; + if (parentDirectory is not null && ShouldSearchParentDirectory(parentDirectory)) { - yield return currentDirectory; - currentDirectory = currentDirectory.Parent; + yield return parentDirectory; } } - private static bool SetUnknownToolchain(out TypeScriptAppHostToolchain toolchain) + internal static bool ShouldSearchParentDirectory(DirectoryInfo parentDirectory, string? homeDirectory = null) { - toolchain = default; - return false; + var pathComparison = OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + // Root and home directories are not project folders. They can contain unrelated user-level + // files, so package manager markers there should not influence TypeScript AppHost projects. + var parentPath = Path.TrimEndingDirectorySeparator(parentDirectory.FullName); + if (string.Equals(parentPath, Path.TrimEndingDirectorySeparator(parentDirectory.Root.FullName), pathComparison)) + { + return false; + } + + homeDirectory ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return string.IsNullOrWhiteSpace(homeDirectory) || + !string.Equals(parentPath, Path.TrimEndingDirectorySeparator(Path.GetFullPath(homeDirectory)), pathComparison); + } + + private static TypeScriptAppHostToolchainResolution CreateLockFileResolution(TypeScriptAppHostToolchain toolchain, string markerName, DirectoryInfo directory) + { + return new(toolchain, $"{markerName} found in {directory.FullName}"); } } + +internal readonly record struct TypeScriptAppHostToolchainResolution(TypeScriptAppHostToolchain Toolchain, string Reason); diff --git a/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs b/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs index 043be567eda..bc78e9d6097 100644 --- a/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs +++ b/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs @@ -181,7 +181,7 @@ private async Task InstallDependenciesAsync( var runtimeSpec = await rpcClient.GetRuntimeSpecAsync(language.LanguageId.Value, cancellationToken); if (TypeScriptAppHostToolchainResolver.IsTypeScriptLanguage(language)) { - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory, _logger); runtimeSpec = TypeScriptAppHostToolchainResolver.ApplyToRuntimeSpec(runtimeSpec, toolchain); } @@ -280,14 +280,14 @@ private static bool IsTypeScriptLanguage(LanguageInfo language) language.LanguageId.Value.Equals(KnownLanguageId.TypeScriptAlias, StringComparison.OrdinalIgnoreCase); } - private static string GetPackageManagerCommand(DirectoryInfo directory, LanguageInfo language) + private string GetPackageManagerCommand(DirectoryInfo directory, LanguageInfo language) { if (!TypeScriptAppHostToolchainResolver.IsTypeScriptLanguage(language)) { return "npm"; } - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory, _logger); return TypeScriptAppHostToolchainResolver.GetCommandName(toolchain); } } diff --git a/src/Aspire.Cli/Utils/EnvironmentChecker/TypeScriptAppHostToolingCheck.cs b/src/Aspire.Cli/Utils/EnvironmentChecker/TypeScriptAppHostToolingCheck.cs index abf8a8bab67..b6ccdadbce8 100644 --- a/src/Aspire.Cli/Utils/EnvironmentChecker/TypeScriptAppHostToolingCheck.cs +++ b/src/Aspire.Cli/Utils/EnvironmentChecker/TypeScriptAppHostToolingCheck.cs @@ -48,7 +48,7 @@ public async Task> CheckAsync(Cancellation return []; } - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(appHostDirectory); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(appHostDirectory, _logger); var missingResults = new List(); foreach (var command in TypeScriptAppHostToolchainResolver.GetRequiredCommands(toolchain)) diff --git a/src/Aspire.Cli/Utils/MissingJavaScriptToolWarning.cs b/src/Aspire.Cli/Utils/MissingJavaScriptToolWarning.cs index 16486bfc877..37f6a935bd8 100644 --- a/src/Aspire.Cli/Utils/MissingJavaScriptToolWarning.cs +++ b/src/Aspire.Cli/Utils/MissingJavaScriptToolWarning.cs @@ -41,7 +41,7 @@ private static (string InstallCommand, string InstallDisplayName) GetMessagePart { if (TypeScriptAppHostToolchainResolver.IsTypeScriptLanguage(language)) { - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(directory, logger: null); return (TypeScriptAppHostToolchainResolver.GetInstallCommand(toolchain), TypeScriptAppHostToolchainResolver.GetDisplayName(toolchain)); } diff --git a/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs b/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs index d49e982eceb..684ee975006 100644 --- a/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs @@ -4,6 +4,8 @@ using Aspire.Cli.Projects; using Aspire.Cli.Tests.Utils; using Aspire.TypeSystem; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; namespace Aspire.Cli.Tests.Projects; @@ -15,7 +17,7 @@ public void Resolve_WhenPackageManagerIsBun_ReturnsBun() using var workspace = TemporaryWorkspace.Create(outputHelper); File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"packageManager\": \"bun@1.2.0\" }"); - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger: null); Assert.Equal(TypeScriptAppHostToolchain.Bun, toolchain); } @@ -27,21 +29,52 @@ public void Resolve_WhenPnpmLockExists_ReturnsPnpm() File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"name\": \"apphost\" }"); File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "pnpm-lock.yaml"), "lockfileVersion: '9.0'"); - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger: null); Assert.Equal(TypeScriptAppHostToolchain.Pnpm, toolchain); } [Fact] - public void Resolve_WhenYarnDirectoryExists_ReturnsYarn() + public void Resolve_WhenPackageLockExists_ReturnsNpm() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var appHostDirectory = workspace.WorkspaceRoot.CreateSubdirectory("apps").CreateSubdirectory("apphost"); + var parentDirectory = appHostDirectory.Parent!; + File.WriteAllText(Path.Combine(parentDirectory.FullName, "package.json"), "{ \"name\": \"workspace\" }"); + File.WriteAllText(Path.Combine(parentDirectory.FullName, "yarn.lock"), string.Empty); + File.WriteAllText(Path.Combine(appHostDirectory.FullName, "package.json"), "{ \"name\": \"apphost\" }"); + File.WriteAllText(Path.Combine(appHostDirectory.FullName, "package-lock.json"), "{}"); + + var resolution = TypeScriptAppHostToolchainResolver.ResolveWithReason(appHostDirectory); + + Assert.Equal(TypeScriptAppHostToolchain.Npm, resolution.Toolchain); + Assert.Equal($"package-lock.json found in {appHostDirectory.FullName}", resolution.Reason); + } + + [Fact] + public void Resolve_WhenPackageLockAndYarnLockExistInSameDirectory_ReturnsYarn() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"name\": \"apphost\" }"); + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package-lock.json"), "{}"); + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "yarn.lock"), string.Empty); + + var resolution = TypeScriptAppHostToolchainResolver.ResolveWithReason(workspace.WorkspaceRoot); + + Assert.Equal(TypeScriptAppHostToolchain.Yarn, resolution.Toolchain); + Assert.Equal($"yarn.lock found in {workspace.WorkspaceRoot.FullName}", resolution.Reason); + } + + [Fact] + public void Resolve_WhenYarnDirectoryExists_ReturnsNpm() { using var workspace = TemporaryWorkspace.Create(outputHelper); File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"name\": \"apphost\" }"); Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, ".yarn")); - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger: null); - Assert.Equal(TypeScriptAppHostToolchain.Yarn, toolchain); + Assert.Equal(TypeScriptAppHostToolchain.Npm, toolchain); } [Fact] @@ -50,42 +83,91 @@ public void Resolve_WhenNothingConfigured_ReturnsNpm() using var workspace = TemporaryWorkspace.Create(outputHelper); File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"name\": \"apphost\" }"); - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger: null); Assert.Equal(TypeScriptAppHostToolchain.Npm, toolchain); } [Fact] - public void Resolve_WhenWorkspaceRootDefinesToolchain_ReturnsParentToolchain() + public void Resolve_WhenMarkerExists_LogsReason() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"name\": \"apphost\" }"); + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "yarn.lock"), string.Empty); + + var sink = new TestSink(); + var logger = new TestLogger(nameof(TypeScriptAppHostToolchainResolverTests), sink, logLevel => logLevel == LogLevel.Debug); + + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger); + + Assert.Equal(TypeScriptAppHostToolchain.Yarn, toolchain); + var write = Assert.Single(sink.Writes); + Assert.Equal(LogLevel.Debug, write.LogLevel); + Assert.Equal($"Selected TypeScript AppHost package manager 'yarn' because yarn.lock found in {workspace.WorkspaceRoot.FullName}.", write.Message); + } + + [Fact] + public void Resolve_WhenParentDirectoryDefinesToolchain_ReturnsParentToolchain() { using var workspace = TemporaryWorkspace.Create(outputHelper); - File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"packageManager\": \"pnpm@10.12.1\" }"); var appHostDirectory = workspace.WorkspaceRoot.CreateSubdirectory("apps").CreateSubdirectory("apphost"); + File.WriteAllText(Path.Combine(appHostDirectory.Parent!.FullName, "package.json"), "{ \"packageManager\": \"pnpm@10.12.1\" }"); File.WriteAllText(Path.Combine(appHostDirectory.FullName, "package.json"), "{ \"name\": \"apphost\" }"); - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(appHostDirectory); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(appHostDirectory, logger: null); Assert.Equal(TypeScriptAppHostToolchain.Pnpm, toolchain); } [Fact] - public void Resolve_WhenToolchainConfigurationIsBeyondMaxDepth_ReturnsNpm() + public void Resolve_WhenGrandparentDirectoryDefinesToolchain_ReturnsNpm() { using var workspace = TemporaryWorkspace.Create(outputHelper); File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"packageManager\": \"bun@1.2.0\" }"); - var currentDirectory = workspace.WorkspaceRoot; - for (var i = 0; i < TypeScriptAppHostToolchainResolver.MaxParentSearchDepth + 1; i++) - { - currentDirectory = currentDirectory.CreateSubdirectory($"level{i}"); - } + var appHostDirectory = workspace.WorkspaceRoot.CreateSubdirectory("apps").CreateSubdirectory("apphost"); - var toolchain = TypeScriptAppHostToolchainResolver.Resolve(currentDirectory); + var toolchain = TypeScriptAppHostToolchainResolver.Resolve(appHostDirectory, logger: null); Assert.Equal(TypeScriptAppHostToolchain.Npm, toolchain); } + [Fact] + public void ShouldSearchParentDirectory_WhenDirectoryIsRoot_ReturnsFalse() + { + var directory = new DirectoryInfo(Path.GetPathRoot(Path.GetTempPath())!); + + var shouldSearch = TypeScriptAppHostToolchainResolver.ShouldSearchParentDirectory(directory); + + Assert.False(shouldSearch); + } + + [Fact] + public void ShouldSearchParentDirectory_WhenDirectoryIsHome_ReturnsFalse() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var shouldSearch = TypeScriptAppHostToolchainResolver.ShouldSearchParentDirectory( + workspace.WorkspaceRoot, + workspace.WorkspaceRoot.FullName); + + Assert.False(shouldSearch); + } + + [Fact] + public void ShouldSearchParentDirectory_WhenDirectoryIsHomeWithDifferentCasingOnCaseInsensitiveOS_ReturnsFalse() + { + Assert.SkipUnless(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS(), "Case-insensitive path comparison only applies to Windows and macOS."); + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var shouldSearch = TypeScriptAppHostToolchainResolver.ShouldSearchParentDirectory( + workspace.WorkspaceRoot, + InvertCasing(workspace.WorkspaceRoot.FullName)); + + Assert.False(shouldSearch); + } + [Fact] public void ApplyToRuntimeSpec_WhenBunSelected_UsesBunCommandsAndPreservesExtensionLaunch() { @@ -144,4 +226,9 @@ private static RuntimeSpec CreateBaseRuntimeSpec() ExtensionLaunchCapability = "node" }; } + + private static string InvertCasing(string value) + { + return new string(value.Select(c => char.IsUpper(c) ? char.ToLowerInvariant(c) : char.ToUpperInvariant(c)).ToArray()); + } } From 82bfeb0d10b3b4770593f5dd4597e281bc09af71 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:08:06 -0700 Subject: [PATCH 06/55] [release/13.3] Fix Windows detached AppHost launcher (#16572) * Fix Windows detached AppHost launcher Use the Windows DETACHED_PROCESS creation flag when launching the detached child CLI process so aspire start is not tied to the launching console lifetime. Keep the existing new process group and restricted handle inheritance behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify Windows detach launcher comment Document that the detached Windows flag combination follows established daemonization patterns used by libuv/Node.js and GitHub CLI, without over-claiming Docker parity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove Windows detach unit test Remove the unit test coverage for the Windows detached process creation flags while keeping the implementation change and manual repro validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: David Fowler Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Processes/DetachedProcessLauncher.Windows.cs | 10 +++++++--- src/Aspire.Cli/Processes/DetachedProcessLauncher.cs | 13 ++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs b/src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs index 04cbb853ec3..4e8197d1fb7 100644 --- a/src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs +++ b/src/Aspire.Cli/Processes/DetachedProcessLauncher.Windows.cs @@ -13,8 +13,9 @@ namespace Aspire.Cli.Processes; internal static partial class DetachedProcessLauncher { /// - /// Windows implementation using CreateProcess with STARTUPINFOEX and - /// PROC_THREAD_ATTRIBUTE_HANDLE_LIST to prevent handle inheritance to grandchildren. + /// Windows implementation using CreateProcess with DETACHED_PROCESS, + /// STARTUPINFOEX, and PROC_THREAD_ATTRIBUTE_HANDLE_LIST to detach from + /// the launching console and prevent handle inheritance to grandchildren. /// [SupportedOSPlatform("windows")] private static Process StartWindows(string fileName, IReadOnlyList arguments, string workingDirectory, Func? shouldRemoveEnvironmentVariable, IReadOnlyDictionary? additionalEnvironmentVariables) @@ -87,7 +88,7 @@ private static Process StartWindows(string fileName, IReadOnlyList argum // Build the command line string: "fileName" arg1 arg2 ... var commandLine = BuildCommandLine(fileName, arguments); - var flags = CreateUnicodeEnvironment | ExtendedStartupInfoPresent | CreateNewProcessGroup; + var flags = WindowsDetachedProcessCreationFlags; // Build a custom environment block if variables need to be removed or added. // CreateProcessW with lpEnvironment=nint.Zero inherits the parent's @@ -307,7 +308,10 @@ private static nint BuildCustomEnvironmentBlock(Func? shouldRemove private const uint StartfUseShowWindow = 0x00000001; private const uint CreateUnicodeEnvironment = 0x00000400; private const uint ExtendedStartupInfoPresent = 0x00080000; + private const uint DetachedProcess = 0x00000008; private const uint CreateNewProcessGroup = 0x00000200; + internal const uint WindowsDetachedProcessCreationFlags = + CreateUnicodeEnvironment | ExtendedStartupInfoPresent | CreateNewProcessGroup | DetachedProcess; private const ushort ShowWindowHide = 0x0000; private static readonly nint s_procThreadAttributeHandleList = (nint)0x00020002; diff --git a/src/Aspire.Cli/Processes/DetachedProcessLauncher.cs b/src/Aspire.Cli/Processes/DetachedProcessLauncher.cs index 805ce5909a7..69cbf5acfab 100644 --- a/src/Aspire.Cli/Processes/DetachedProcessLauncher.cs +++ b/src/Aspire.Cli/Processes/DetachedProcessLauncher.cs @@ -36,13 +36,16 @@ namespace Aspire.Cli.Processes; // The solution is platform-specific: // // ┌─────────┬────────────────────────────────────────────────────────────────┐ -// │ Windows │ P/Invoke CreateProcess with STARTUPINFOEX and an explicit │ -// │ │ PROC_THREAD_ATTRIBUTE_HANDLE_LIST. This lets us set │ +// │ Windows │ P/Invoke CreateProcess with DETACHED_PROCESS, │ +// │ │ STARTUPINFOEX, and an explicit │ +// │ │ PROC_THREAD_ATTRIBUTE_HANDLE_LIST. This detaches the child │ +// │ │ from the launching console while still letting us set │ // │ │ bInheritHandles=TRUE (required to assign hStdOutput to NUL) │ -// │ │ while restricting inheritance to ONLY the NUL handle — so the │ +// │ │ and restrict inheritance to ONLY the NUL handle — so the │ // │ │ grandchild inherits nothing useful. Child stdout/stderr go to │ -// │ │ the NUL device. This is the same approach used by Docker's │ -// │ │ Windows container runtime (microsoft/hcsshim). │ +// │ │ the NUL device. The detached flag combination matches │ +// │ │ established Windows daemonization patterns used by tools such │ +// │ │ as libuv/Node.js and GitHub CLI. │ // │ │ │ // │ Linux / │ Process.Start with RedirectStandard{Output,Error} = true, │ // │ macOS │ then immediately close the parent's read-end pipe streams. │ From c108d40157b13e79d9865debc2fcf41515e12a7d Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:34:27 -0700 Subject: [PATCH 07/55] [release/13.3] Validate build-only container references in the pipeline (#16582) * Validate build-only container references in the pipeline Add the publish/deploy validation step and implement the opt-out by clearing its RequiredBySteps during pipeline configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix tests * PR feedback Move DisableBuildOnlyContainerValidation to the Pipeline. * PR feedback * Fix tests * Address build-only container validation feedback Ensure manifest publishing runs the build-only container validation step and strengthen tests to cover mixed consumed and unconsumed build-only containers. * Revert publish-manifest changes. * Apply suggestion from @eerhardt --------- Co-authored-by: Eric Erhardt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DistributedApplicationPipeline.cs | 56 ++++ ...istributedApplicationPipelineExtensions.cs | 38 +++ .../AzureDeployerTests.cs | 1 + ..._DoesNotHang_step=diagnostics.verified.txt | 175 +++++++------ ...nments_Works_step=diagnostics.verified.txt | 235 +++++++++-------- ...ts_CreatesCorrectDependencies.verified.txt | 247 +++++++++--------- ...on_CreatesCorrectDependencies.verified.txt | 217 +++++++-------- ...TwoPassScanningGeneratedAspire.verified.go | 12 + ...oPassScanningGeneratedAspire.verified.java | 7 + ...TwoPassScanningGeneratedAspire.verified.py | 9 + ...TwoPassScanningGeneratedAspire.verified.rs | 9 + ...TwoPassScanningGeneratedAspire.verified.ts | 22 ++ .../BuildOnlyContainerValidationTests.cs | 122 +++++++++ .../DistributedApplicationPipelineTests.cs | 3 +- 14 files changed, 737 insertions(+), 416 deletions(-) create mode 100644 src/Aspire.Hosting/Pipelines/DistributedApplicationPipelineExtensions.cs create mode 100644 tests/Aspire.Hosting.Tests/BuildOnlyContainerValidationTests.cs diff --git a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs index 41ea625621a..100f6e3d9f9 100644 --- a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs +++ b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs @@ -24,6 +24,8 @@ namespace Aspire.Hosting.Pipelines; [DebuggerDisplay("{ToString(),nq}")] internal sealed class DistributedApplicationPipeline : IDistributedApplicationPipeline { + internal const string ValidateBuildOnlyContainerReferencesStepName = "validate-build-only-container-references"; + private readonly List _steps = []; private readonly List> _configurationCallbacks = []; @@ -310,6 +312,18 @@ public DistributedApplicationPipeline() Action = _ => Task.CompletedTask, }); + _steps.Add(new PipelineStep + { + Name = ValidateBuildOnlyContainerReferencesStepName, + Description = "Validates that build-only containers are consumed by another resource before publish or deploy.", + Action = static context => + { + ValidateBuildOnlyContainerReferences(context.Model); + return Task.CompletedTask; + }, + RequiredBySteps = [WellKnownPipelineSteps.PublishPrereq, WellKnownPipelineSteps.DeployPrereq], + }); + // Add diagnostic step for dependency graph analysis _steps.Add(new PipelineStep { @@ -398,6 +412,48 @@ private static void ValidateComputeEnvironmentBindings(DistributedApplicationMod $"Specify which environment each resource should target by calling 'WithComputeEnvironment' on the resource builder."); } + private static void ValidateBuildOnlyContainerReferences(DistributedApplicationModel model) + { + var buildOnlyContainers = model.GetBuildResources() + .Where(resource => resource.IsBuildOnlyContainer() && !resource.IsExcludedFromPublish()) + .ToList(); + + if (buildOnlyContainers.Count == 0) + { + return; + } + + var consumedBuildOnlyContainerNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var resource in model.Resources) + { + if (resource.IsExcludedFromPublish()) + { + continue; + } + + foreach (var annotation in resource.Annotations.OfType()) + { + if (!string.Equals(resource.Name, annotation.Source.Name, StringComparison.OrdinalIgnoreCase)) + { + consumedBuildOnlyContainerNames.Add(annotation.Source.Name); + } + } + } + + var unconsumedBuildOnlyContainers = buildOnlyContainers + .Where(resource => !consumedBuildOnlyContainerNames.Contains(resource.Name)) + .ToList(); + + if (unconsumedBuildOnlyContainers.Count > 0) + { + var resourceNames = string.Join("', '", unconsumedBuildOnlyContainers.Select(resource => resource.Name)); + throw new DistributedApplicationException( + $"Build-only container resource(s) '{resourceNames}' are not consumed by another resource and won't participate in publish or deploy. " + + $"Reference them from another resource, for example using 'PublishWithContainerFiles' or 'PublishWithStaticFiles', or suppress this validation for the app by calling 'builder.Pipeline.DisableBuildOnlyContainerValidation()'."); + } + } + public bool HasSteps => _steps.Count > 0; public void AddStep(string name, diff --git a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipelineExtensions.cs b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipelineExtensions.cs new file mode 100644 index 00000000000..f706245b5b8 --- /dev/null +++ b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipelineExtensions.cs @@ -0,0 +1,38 @@ +// 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; + +namespace Aspire.Hosting.Pipelines; + +/// +/// Extension methods for . +/// +[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +public static class DistributedApplicationPipelineExtensions +{ + /// + /// Disables the publish and deploy validation that requires build-only containers to be consumed by another resource. + /// + /// The distributed application pipeline. + /// The distributed application pipeline for chaining. + /// + /// This is an application-wide escape hatch for scenarios where the build-only container validation is too restrictive + /// for a particular app. Prefer wiring build-only containers through PublishWithContainerFiles or + /// PublishWithStaticFiles when possible. + /// + [AspireExport(Description = "Disables publish and deploy validation for unconsumed build-only containers.")] + public static IDistributedApplicationPipeline DisableBuildOnlyContainerValidation(this IDistributedApplicationPipeline pipeline) + { + ArgumentNullException.ThrowIfNull(pipeline); + + pipeline.AddPipelineConfiguration(static context => + { + var validationStep = context.Steps.SingleOrDefault(step => step.Name == DistributedApplicationPipeline.ValidateBuildOnlyContainerReferencesStepName); + validationStep?.RequiredBySteps.Clear(); + return Task.CompletedTask; + }); + + return pipeline; + } +} diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index 98833afb486..55fa2b34b5c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -158,6 +158,7 @@ public async Task DeployAsync_WithBuildOnlyContainers() }; }); ConfigureTestServices(builder, armClientProvider: armClientProvider, activityReporter: mockActivityReporter); + builder.Pipeline.DisableBuildOnlyContainerValidation(); var containerAppEnv = builder.AddAzureContainerAppEnvironment("env"); diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt index a21d8c91380..dc2c4411a36 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt @@ -5,7 +5,7 @@ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS This diagnostic output shows the complete pipeline dependency graph structure. Use this to understand step relationships and troubleshoot execution issues. -Total steps defined: 37 +Total steps defined: 38 Analysis for full pipeline execution (showing all steps and their relationships) @@ -22,35 +22,36 @@ Steps with no dependencies run first, followed by steps that depend on them. 6. process-parameters 7. build-prereq 8. check-container-runtime - 9. deploy-prereq - 10. build-api - 11. build - 12. validate-azure-login - 13. create-provisioning-context - 14. provision-api-identity - 15. provision-kv - 16. provision-api-roles-kv - 17. provision-env-acr - 18. provision-env - 19. login-to-acr-env-acr - 20. push-prereq - 21. push-api - 22. provision-api-website - 23. print-api-summary - 24. provision-azure-bicep-resources - 25. print-dashboard-url-env - 26. deploy - 27. deploy-api - 28. destroy-prereq - 29. destroy-azure-azure634f9 - 30. destroy - 31. diagnostics - 32. publish-prereq - 33. publish-azure634f9 - 34. validate-appservice-config-env - 35. publish - 36. publish-manifest - 37. push + 9. validate-build-only-container-references + 10. deploy-prereq + 11. build-api + 12. build + 13. validate-azure-login + 14. create-provisioning-context + 15. provision-api-identity + 16. provision-kv + 17. provision-api-roles-kv + 18. provision-env-acr + 19. provision-env + 20. login-to-acr-env-acr + 21. push-prereq + 22. push-api + 23. provision-api-website + 24. print-api-summary + 25. provision-azure-bicep-resources + 26. print-dashboard-url-env + 27. deploy + 28. deploy-api + 29. destroy-prereq + 30. destroy-azure-azure634f9 + 31. destroy + 32. diagnostics + 33. publish-prereq + 34. publish-azure634f9 + 35. validate-appservice-config-env + 36. publish + 37. publish-manifest + 38. push DETAILED STEP ANALYSIS ====================== @@ -101,7 +102,7 @@ Step: deploy-api Step: deploy-prereq Description: Prerequisite step that runs before any deploy operations. Initializes deployment environment and manages deployment state. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: destroy Description: Aggregation step for all destroy operations. All destroy steps should be required by this step. @@ -203,7 +204,7 @@ Step: publish-manifest Step: publish-prereq Description: Prerequisite step that runs before any publish operations. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: push Description: Aggregation step for all push operations. All push steps should be required by this step. @@ -231,6 +232,10 @@ Step: validate-azure-login Dependencies: ✓ deploy-prereq Resource: azure634f9 (AzureEnvironmentResource) +Step: validate-build-only-container-references + Description: Validates that build-only containers are consumed by another resource before publish or deploy. + Dependencies: none + Step: validate-compute-environments Description: Validates compute resource bindings before startup. Dependencies: none @@ -262,18 +267,18 @@ If targeting 'before-start': If targeting 'build': Direct dependencies: build-api - Total steps: 6 + Total steps: 7 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api [3] build If targeting 'build-api': Direct dependencies: build-prereq, check-container-runtime, deploy-prereq - Total steps: 5 + Total steps: 6 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api @@ -292,18 +297,18 @@ If targeting 'check-container-runtime': If targeting 'create-provisioning-context': Direct dependencies: deploy-prereq, validate-azure-login - Total steps: 4 + Total steps: 5 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context If targeting 'deploy': Direct dependencies: build-api, create-provisioning-context, print-api-summary, print-dashboard-url-env, provision-azure-bicep-resources, validate-azure-login - Total steps: 20 + Total steps: 21 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -318,9 +323,9 @@ If targeting 'deploy': If targeting 'deploy-api': Direct dependencies: print-api-summary - Total steps: 18 + Total steps: 19 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -333,10 +338,10 @@ If targeting 'deploy-api': [10] deploy-api If targeting 'deploy-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq If targeting 'destroy': @@ -368,9 +373,9 @@ If targeting 'diagnostics': If targeting 'login-to-acr-env-acr': Direct dependencies: provision-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -386,9 +391,9 @@ If targeting 'prepare-azure-app-service-env': If targeting 'print-api-summary': Direct dependencies: provision-api-website - Total steps: 17 + Total steps: 18 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -401,9 +406,9 @@ If targeting 'print-api-summary': If targeting 'print-dashboard-url-env': Direct dependencies: provision-azure-bicep-resources, provision-env - Total steps: 18 + Total steps: 19 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -423,9 +428,9 @@ If targeting 'process-parameters': If targeting 'provision-api-identity': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -433,9 +438,9 @@ If targeting 'provision-api-identity': If targeting 'provision-api-roles-kv': Direct dependencies: create-provisioning-context, provision-api-identity, provision-kv - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -444,9 +449,9 @@ If targeting 'provision-api-roles-kv': If targeting 'provision-api-website': Direct dependencies: create-provisioning-context, provision-api-identity, provision-api-roles-kv, provision-env, provision-kv, push-api - Total steps: 16 + Total steps: 17 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -458,9 +463,9 @@ If targeting 'provision-api-website': If targeting 'provision-azure-bicep-resources': Direct dependencies: create-provisioning-context, deploy-prereq, provision-api-identity, provision-api-roles-kv, provision-api-website, provision-env, provision-env-acr, provision-kv - Total steps: 17 + Total steps: 18 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -473,9 +478,9 @@ If targeting 'provision-azure-bicep-resources': If targeting 'provision-env': Direct dependencies: create-provisioning-context, provision-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -484,9 +489,9 @@ If targeting 'provision-env': If targeting 'provision-env-acr': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -494,9 +499,9 @@ If targeting 'provision-env-acr': If targeting 'provision-kv': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -504,18 +509,18 @@ If targeting 'provision-kv': If targeting 'publish': Direct dependencies: publish-azure634f9, validate-appservice-config-env - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 | validate-appservice-config-env (parallel) [3] publish If targeting 'publish-azure634f9': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 @@ -526,17 +531,17 @@ If targeting 'publish-manifest': [0] publish-manifest If targeting 'publish-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq If targeting 'push': Direct dependencies: push-api, push-prereq - Total steps: 12 + Total steps: 13 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -548,9 +553,9 @@ If targeting 'push': If targeting 'push-api': Direct dependencies: build-api, push-prereq - Total steps: 11 + Total steps: 12 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -561,9 +566,9 @@ If targeting 'push-api': If targeting 'push-prereq': Direct dependencies: login-to-acr-env-acr - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -573,9 +578,9 @@ If targeting 'push-prereq': If targeting 'validate-appservice-config-env': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] validate-appservice-config-env @@ -587,12 +592,18 @@ If targeting 'validate-azure-app-service': If targeting 'validate-azure-login': Direct dependencies: deploy-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login +If targeting 'validate-build-only-container-references': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] validate-build-only-container-references + If targeting 'validate-compute-environments': Direct dependencies: none Total steps: 1 diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt index 531661150c9..142023e9c37 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt @@ -5,7 +5,7 @@ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS This diagnostic output shows the complete pipeline dependency graph structure. Use this to understand step relationships and troubleshoot execution issues. -Total steps defined: 49 +Total steps defined: 50 Analysis for full pipeline execution (showing all steps and their relationships) @@ -24,45 +24,46 @@ Steps with no dependencies run first, followed by steps that depend on them. 8. process-parameters 9. build-prereq 10. check-container-runtime - 11. deploy-prereq - 12. build-api-service - 13. build-python-app - 14. build - 15. validate-azure-login - 16. create-provisioning-context - 17. provision-aas-env-acr - 18. provision-aas-env - 19. login-to-acr-aas-env-acr - 20. provision-aca-env-acr - 21. login-to-acr-aca-env-acr - 22. push-prereq - 23. push-api-service - 24. provision-api-service-website - 25. print-api-service-summary - 26. provision-aca-env - 27. provision-cache-containerapp - 28. print-cache-summary - 29. push-python-app - 30. provision-python-app-containerapp - 31. provision-storage - 32. provision-azure-bicep-resources - 33. print-dashboard-url-aas-env - 34. print-dashboard-url-aca-env - 35. print-python-app-summary - 36. deploy - 37. deploy-api-service - 38. deploy-cache - 39. deploy-python-app - 40. destroy-prereq - 41. destroy-azure-azure634f9 - 42. destroy - 43. diagnostics - 44. publish-prereq - 45. publish-azure634f9 - 46. validate-appservice-config-aas-env - 47. publish - 48. publish-manifest - 49. push + 11. validate-build-only-container-references + 12. deploy-prereq + 13. build-api-service + 14. build-python-app + 15. build + 16. validate-azure-login + 17. create-provisioning-context + 18. provision-aas-env-acr + 19. provision-aas-env + 20. login-to-acr-aas-env-acr + 21. provision-aca-env-acr + 22. login-to-acr-aca-env-acr + 23. push-prereq + 24. push-api-service + 25. provision-api-service-website + 26. print-api-service-summary + 27. provision-aca-env + 28. provision-cache-containerapp + 29. print-cache-summary + 30. push-python-app + 31. provision-python-app-containerapp + 32. provision-storage + 33. provision-azure-bicep-resources + 34. print-dashboard-url-aas-env + 35. print-dashboard-url-aca-env + 36. print-python-app-summary + 37. deploy + 38. deploy-api-service + 39. deploy-cache + 40. deploy-python-app + 41. destroy-prereq + 42. destroy-azure-azure634f9 + 43. destroy + 44. diagnostics + 45. publish-prereq + 46. publish-azure634f9 + 47. validate-appservice-config-aas-env + 48. publish + 49. publish-manifest + 50. push DETAILED STEP ANALYSIS ====================== @@ -124,7 +125,7 @@ Step: deploy-cache Step: deploy-prereq Description: Prerequisite step that runs before any deploy operations. Initializes deployment environment and manages deployment state. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: deploy-python-app Description: Aggregation step for deploying python-app to Azure Container Apps. @@ -272,7 +273,7 @@ Step: publish-manifest Step: publish-prereq Description: Prerequisite step that runs before any publish operations. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: push Description: Aggregation step for all push operations. All push steps should be required by this step. @@ -308,6 +309,10 @@ Step: validate-azure-login Dependencies: ✓ deploy-prereq Resource: azure634f9 (AzureEnvironmentResource) +Step: validate-build-only-container-references + Description: Validates that build-only containers are consumed by another resource before publish or deploy. + Dependencies: none + Step: validate-compute-environments Description: Validates compute resource bindings before startup. Dependencies: none @@ -339,18 +344,18 @@ If targeting 'before-start': If targeting 'build': Direct dependencies: build-api-service, build-python-app - Total steps: 7 + Total steps: 8 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app (parallel) [3] build If targeting 'build-api-service': Direct dependencies: build-prereq, check-container-runtime, deploy-prereq, deploy-prereq - Total steps: 5 + Total steps: 6 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service @@ -363,9 +368,9 @@ If targeting 'build-prereq': If targeting 'build-python-app': Direct dependencies: build-prereq, check-container-runtime, deploy-prereq, deploy-prereq - Total steps: 5 + Total steps: 6 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-python-app @@ -377,18 +382,18 @@ If targeting 'check-container-runtime': If targeting 'create-provisioning-context': Direct dependencies: deploy-prereq, validate-azure-login - Total steps: 4 + Total steps: 5 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context If targeting 'deploy': Direct dependencies: build-api-service, build-python-app, create-provisioning-context, print-api-service-summary, print-cache-summary, print-dashboard-url-aas-env, print-dashboard-url-aca-env, print-python-app-summary, provision-azure-bicep-resources, validate-azure-login - Total steps: 28 + Total steps: 29 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -403,9 +408,9 @@ If targeting 'deploy': If targeting 'deploy-api-service': Direct dependencies: print-api-service-summary - Total steps: 17 + Total steps: 18 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context @@ -419,9 +424,9 @@ If targeting 'deploy-api-service': If targeting 'deploy-cache': Direct dependencies: print-cache-summary - Total steps: 9 + Total steps: 10 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -432,17 +437,17 @@ If targeting 'deploy-cache': [8] deploy-cache If targeting 'deploy-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq If targeting 'deploy-python-app': Direct dependencies: print-python-app-summary - Total steps: 17 + Total steps: 18 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -483,9 +488,9 @@ If targeting 'diagnostics': If targeting 'login-to-acr-aas-env-acr': Direct dependencies: provision-aas-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -494,9 +499,9 @@ If targeting 'login-to-acr-aas-env-acr': If targeting 'login-to-acr-aca-env-acr': Direct dependencies: provision-aca-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -519,9 +524,9 @@ If targeting 'prepare-azure-container-apps-aca-env': If targeting 'print-api-service-summary': Direct dependencies: provision-api-service-website - Total steps: 16 + Total steps: 17 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context @@ -534,9 +539,9 @@ If targeting 'print-api-service-summary': If targeting 'print-cache-summary': Direct dependencies: provision-cache-containerapp - Total steps: 8 + Total steps: 9 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -547,9 +552,9 @@ If targeting 'print-cache-summary': If targeting 'print-dashboard-url-aas-env': Direct dependencies: provision-aas-env, provision-azure-bicep-resources - Total steps: 23 + Total steps: 24 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -563,9 +568,9 @@ If targeting 'print-dashboard-url-aas-env': If targeting 'print-dashboard-url-aca-env': Direct dependencies: provision-aca-env, provision-azure-bicep-resources - Total steps: 23 + Total steps: 24 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -579,9 +584,9 @@ If targeting 'print-dashboard-url-aca-env': If targeting 'print-python-app-summary': Direct dependencies: provision-python-app-containerapp - Total steps: 16 + Total steps: 17 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -600,9 +605,9 @@ If targeting 'process-parameters': If targeting 'provision-aas-env': Direct dependencies: create-provisioning-context, provision-aas-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -611,9 +616,9 @@ If targeting 'provision-aas-env': If targeting 'provision-aas-env-acr': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -621,9 +626,9 @@ If targeting 'provision-aas-env-acr': If targeting 'provision-aca-env': Direct dependencies: create-provisioning-context, provision-aca-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -632,9 +637,9 @@ If targeting 'provision-aca-env': If targeting 'provision-aca-env-acr': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -642,9 +647,9 @@ If targeting 'provision-aca-env-acr': If targeting 'provision-api-service-website': Direct dependencies: create-provisioning-context, provision-aas-env, push-api-service - Total steps: 15 + Total steps: 16 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context @@ -656,9 +661,9 @@ If targeting 'provision-api-service-website': If targeting 'provision-azure-bicep-resources': Direct dependencies: create-provisioning-context, deploy-prereq, provision-aas-env, provision-aas-env-acr, provision-aca-env, provision-aca-env-acr, provision-api-service-website, provision-cache-containerapp, provision-python-app-containerapp, provision-storage - Total steps: 22 + Total steps: 23 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -671,9 +676,9 @@ If targeting 'provision-azure-bicep-resources': If targeting 'provision-cache-containerapp': Direct dependencies: create-provisioning-context, provision-aca-env - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -683,9 +688,9 @@ If targeting 'provision-cache-containerapp': If targeting 'provision-python-app-containerapp': Direct dependencies: create-provisioning-context, provision-aca-env, push-python-app - Total steps: 15 + Total steps: 16 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -697,9 +702,9 @@ If targeting 'provision-python-app-containerapp': If targeting 'provision-storage': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -707,18 +712,18 @@ If targeting 'provision-storage': If targeting 'publish': Direct dependencies: publish-azure634f9, validate-appservice-config-aas-env - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 | validate-appservice-config-aas-env (parallel) [3] publish If targeting 'publish-azure634f9': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 @@ -729,17 +734,17 @@ If targeting 'publish-manifest': [0] publish-manifest If targeting 'publish-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq If targeting 'push': Direct dependencies: push-api-service, push-prereq, push-python-app - Total steps: 16 + Total steps: 17 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -751,9 +756,9 @@ If targeting 'push': If targeting 'push-api-service': Direct dependencies: build-api-service, push-prereq - Total steps: 13 + Total steps: 14 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context @@ -764,9 +769,9 @@ If targeting 'push-api-service': If targeting 'push-prereq': Direct dependencies: login-to-acr-aas-env-acr, login-to-acr-aca-env-acr - Total steps: 9 + Total steps: 10 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -776,9 +781,9 @@ If targeting 'push-prereq': If targeting 'push-python-app': Direct dependencies: build-python-app, push-prereq - Total steps: 13 + Total steps: 14 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context @@ -789,9 +794,9 @@ If targeting 'push-python-app': If targeting 'validate-appservice-config-aas-env': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] validate-appservice-config-aas-env @@ -809,12 +814,18 @@ If targeting 'validate-azure-container-apps': If targeting 'validate-azure-login': Direct dependencies: deploy-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login +If targeting 'validate-build-only-container-references': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] validate-build-only-container-references + If targeting 'validate-compute-environments': Direct dependencies: none Total steps: 1 diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt index 2423ce98bca..da7622ad9ca 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithPrivateEndpoints_CreatesCorrectDependencies.verified.txt @@ -5,7 +5,7 @@ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS This diagnostic output shows the complete pipeline dependency graph structure. Use this to understand step relationships and troubleshoot execution issues. -Total steps defined: 49 +Total steps defined: 50 Analysis for full pipeline execution (showing all steps and their relationships) @@ -22,47 +22,48 @@ Steps with no dependencies run first, followed by steps that depend on them. 6. process-parameters 7. build-prereq 8. check-container-runtime - 9. deploy-prereq - 10. build-api - 11. build - 12. validate-azure-login - 13. create-provisioning-context - 14. provision-api-identity - 15. provision-cosmos - 16. provision-api-roles-cosmos - 17. provision-sql-nsg - 18. provision-vnet - 19. provision-privatelink-file-core-windows-net - 20. provision-sql-store - 21. provision-pe-subnet-files-pe - 22. provision-privatelink-database-windows-net - 23. provision-sql - 24. provision-pe-subnet-sql-pe - 25. provision-api-roles-sql - 26. provision-env-acr - 27. provision-env - 28. provision-privatelink-documents-azure-com - 29. provision-pe-subnet-cosmos-pe - 30. login-to-acr-env-acr - 31. push-prereq - 32. push-api - 33. provision-api-containerapp - 34. print-api-summary - 35. provision-sql-admin-identity - 36. provision-sql-admin-identity-roles-sql-store - 37. provision-azure-bicep-resources - 38. print-dashboard-url-env - 39. deploy - 40. deploy-api - 41. destroy-prereq - 42. destroy-azure-azure634f9 - 43. destroy - 44. diagnostics - 45. publish-prereq - 46. publish-azure634f9 - 47. publish - 48. publish-manifest - 49. push + 9. validate-build-only-container-references + 10. deploy-prereq + 11. build-api + 12. build + 13. validate-azure-login + 14. create-provisioning-context + 15. provision-api-identity + 16. provision-cosmos + 17. provision-api-roles-cosmos + 18. provision-sql-nsg + 19. provision-vnet + 20. provision-privatelink-file-core-windows-net + 21. provision-sql-store + 22. provision-pe-subnet-files-pe + 23. provision-privatelink-database-windows-net + 24. provision-sql + 25. provision-pe-subnet-sql-pe + 26. provision-api-roles-sql + 27. provision-env-acr + 28. provision-env + 29. provision-privatelink-documents-azure-com + 30. provision-pe-subnet-cosmos-pe + 31. login-to-acr-env-acr + 32. push-prereq + 33. push-api + 34. provision-api-containerapp + 35. print-api-summary + 36. provision-sql-admin-identity + 37. provision-sql-admin-identity-roles-sql-store + 38. provision-azure-bicep-resources + 39. print-dashboard-url-env + 40. deploy + 41. deploy-api + 42. destroy-prereq + 43. destroy-azure-azure634f9 + 44. destroy + 45. diagnostics + 46. publish-prereq + 47. publish-azure634f9 + 48. publish + 49. publish-manifest + 50. push DETAILED STEP ANALYSIS ====================== @@ -113,7 +114,7 @@ Step: deploy-api Step: deploy-prereq Description: Prerequisite step that runs before any deploy operations. Initializes deployment environment and manages deployment state. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: destroy Description: Aggregation step for all destroy operations. All destroy steps should be required by this step. @@ -293,7 +294,7 @@ Step: publish-manifest Step: publish-prereq Description: Prerequisite step that runs before any publish operations. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: push Description: Aggregation step for all push operations. All push steps should be required by this step. @@ -316,6 +317,10 @@ Step: validate-azure-login Dependencies: ✓ deploy-prereq Resource: azure634f9 (AzureEnvironmentResource) +Step: validate-build-only-container-references + Description: Validates that build-only containers are consumed by another resource before publish or deploy. + Dependencies: none + Step: validate-compute-environments Description: Validates compute resource bindings before startup. Dependencies: none @@ -347,18 +352,18 @@ If targeting 'before-start': If targeting 'build': Direct dependencies: build-api - Total steps: 6 + Total steps: 7 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api [3] build If targeting 'build-api': Direct dependencies: build-prereq, check-container-runtime, deploy-prereq - Total steps: 5 + Total steps: 6 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api @@ -377,18 +382,18 @@ If targeting 'check-container-runtime': If targeting 'create-provisioning-context': Direct dependencies: deploy-prereq, validate-azure-login - Total steps: 4 + Total steps: 5 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context If targeting 'deploy': Direct dependencies: build-api, create-provisioning-context, print-api-summary, print-dashboard-url-env, provision-azure-bicep-resources, validate-azure-login - Total steps: 33 + Total steps: 34 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -404,9 +409,9 @@ If targeting 'deploy': If targeting 'deploy-api': Direct dependencies: print-api-summary - Total steps: 29 + Total steps: 30 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -420,10 +425,10 @@ If targeting 'deploy-api': [11] deploy-api If targeting 'deploy-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq If targeting 'destroy': @@ -455,9 +460,9 @@ If targeting 'diagnostics': If targeting 'login-to-acr-env-acr': Direct dependencies: provision-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -473,9 +478,9 @@ If targeting 'prepare-azure-container-apps-env': If targeting 'print-api-summary': Direct dependencies: provision-api-containerapp - Total steps: 28 + Total steps: 29 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -489,9 +494,9 @@ If targeting 'print-api-summary': If targeting 'print-dashboard-url-env': Direct dependencies: provision-azure-bicep-resources, provision-env - Total steps: 31 + Total steps: 32 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -512,9 +517,9 @@ If targeting 'process-parameters': If targeting 'provision-api-containerapp': Direct dependencies: create-provisioning-context, provision-api-identity, provision-api-roles-cosmos, provision-api-roles-sql, provision-cosmos, provision-env, provision-pe-subnet-cosmos-pe, provision-pe-subnet-sql-pe, provision-sql, push-api - Total steps: 27 + Total steps: 28 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -527,9 +532,9 @@ If targeting 'provision-api-containerapp': If targeting 'provision-api-identity': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -537,9 +542,9 @@ If targeting 'provision-api-identity': If targeting 'provision-api-roles-cosmos': Direct dependencies: create-provisioning-context, provision-api-identity, provision-cosmos - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -548,9 +553,9 @@ If targeting 'provision-api-roles-cosmos': If targeting 'provision-api-roles-sql': Direct dependencies: create-provisioning-context, provision-api-identity, provision-pe-subnet-files-pe, provision-pe-subnet-sql-pe, provision-sql, provision-sql-store, provision-vnet - Total steps: 14 + Total steps: 15 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -562,9 +567,9 @@ If targeting 'provision-api-roles-sql': If targeting 'provision-azure-bicep-resources': Direct dependencies: create-provisioning-context, deploy-prereq, provision-api-containerapp, provision-api-identity, provision-api-roles-cosmos, provision-api-roles-sql, provision-cosmos, provision-env, provision-env-acr, provision-pe-subnet-cosmos-pe, provision-pe-subnet-files-pe, provision-pe-subnet-sql-pe, provision-privatelink-database-windows-net, provision-privatelink-documents-azure-com, provision-privatelink-file-core-windows-net, provision-sql, provision-sql-admin-identity, provision-sql-admin-identity-roles-sql-store, provision-sql-nsg, provision-sql-store, provision-vnet - Total steps: 30 + Total steps: 31 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -578,9 +583,9 @@ If targeting 'provision-azure-bicep-resources': If targeting 'provision-cosmos': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -588,9 +593,9 @@ If targeting 'provision-cosmos': If targeting 'provision-env': Direct dependencies: create-provisioning-context, provision-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -599,9 +604,9 @@ If targeting 'provision-env': If targeting 'provision-env-acr': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -609,9 +614,9 @@ If targeting 'provision-env-acr': If targeting 'provision-pe-subnet-cosmos-pe': Direct dependencies: create-provisioning-context, provision-cosmos, provision-privatelink-documents-azure-com, provision-vnet - Total steps: 9 + Total steps: 10 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -622,9 +627,9 @@ If targeting 'provision-pe-subnet-cosmos-pe': If targeting 'provision-pe-subnet-files-pe': Direct dependencies: create-provisioning-context, provision-privatelink-file-core-windows-net, provision-sql-store, provision-vnet - Total steps: 9 + Total steps: 10 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -635,9 +640,9 @@ If targeting 'provision-pe-subnet-files-pe': If targeting 'provision-pe-subnet-sql-pe': Direct dependencies: create-provisioning-context, provision-privatelink-database-windows-net, provision-sql, provision-vnet - Total steps: 9 + Total steps: 10 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -648,9 +653,9 @@ If targeting 'provision-pe-subnet-sql-pe': If targeting 'provision-privatelink-database-windows-net': Direct dependencies: create-provisioning-context, provision-vnet - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -660,9 +665,9 @@ If targeting 'provision-privatelink-database-windows-net': If targeting 'provision-privatelink-documents-azure-com': Direct dependencies: create-provisioning-context, provision-vnet - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -672,9 +677,9 @@ If targeting 'provision-privatelink-documents-azure-com': If targeting 'provision-privatelink-file-core-windows-net': Direct dependencies: create-provisioning-context, provision-vnet - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -684,9 +689,9 @@ If targeting 'provision-privatelink-file-core-windows-net': If targeting 'provision-sql': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -694,9 +699,9 @@ If targeting 'provision-sql': If targeting 'provision-sql-admin-identity': Direct dependencies: create-provisioning-context, provision-sql - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -705,9 +710,9 @@ If targeting 'provision-sql-admin-identity': If targeting 'provision-sql-admin-identity-roles-sql-store': Direct dependencies: create-provisioning-context, provision-sql-admin-identity, provision-sql-store - Total steps: 8 + Total steps: 9 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -717,9 +722,9 @@ If targeting 'provision-sql-admin-identity-roles-sql-store': If targeting 'provision-sql-nsg': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -727,9 +732,9 @@ If targeting 'provision-sql-nsg': If targeting 'provision-sql-store': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -737,9 +742,9 @@ If targeting 'provision-sql-store': If targeting 'provision-vnet': Direct dependencies: create-provisioning-context, provision-sql-nsg - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -748,18 +753,18 @@ If targeting 'provision-vnet': If targeting 'publish': Direct dependencies: publish-azure634f9 - Total steps: 4 + Total steps: 5 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 [3] publish If targeting 'publish-azure634f9': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 @@ -770,17 +775,17 @@ If targeting 'publish-manifest': [0] publish-manifest If targeting 'publish-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq If targeting 'push': Direct dependencies: push-api, push-prereq - Total steps: 12 + Total steps: 13 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -792,9 +797,9 @@ If targeting 'push': If targeting 'push-api': Direct dependencies: build-api, push-prereq - Total steps: 11 + Total steps: 12 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -805,9 +810,9 @@ If targeting 'push-api': If targeting 'push-prereq': Direct dependencies: login-to-acr-env-acr - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -823,12 +828,18 @@ If targeting 'validate-azure-container-apps': If targeting 'validate-azure-login': Direct dependencies: deploy-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login +If targeting 'validate-build-only-container-references': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] validate-build-only-container-references + If targeting 'validate-compute-environments': Direct dependencies: none Total steps: 1 diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt index c0bcc1c442b..7342c8f260c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt @@ -5,7 +5,7 @@ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS This diagnostic output shows the complete pipeline dependency graph structure. Use this to understand step relationships and troubleshoot execution issues. -Total steps defined: 44 +Total steps defined: 45 Analysis for full pipeline execution (showing all steps and their relationships) @@ -22,42 +22,43 @@ Steps with no dependencies run first, followed by steps that depend on them. 6. process-parameters 7. build-prereq 8. check-container-runtime - 9. deploy-prereq - 10. build-api - 11. build - 12. validate-azure-login - 13. create-provisioning-context - 14. provision-api-identity - 15. provision-cache-kv - 16. provision-api-roles-cache-kv - 17. provision-cosmos-kv - 18. provision-api-roles-cosmos-kv - 19. provision-pg-kv - 20. provision-api-roles-pg-kv - 21. provision-cache - 22. provision-cosmos - 23. provision-env-acr - 24. provision-env - 25. provision-pg - 26. login-to-acr-env-acr - 27. push-prereq - 28. push-api - 29. provision-api-website - 30. print-api-summary - 31. provision-azure-bicep-resources - 32. print-dashboard-url-env - 33. deploy - 34. deploy-api - 35. destroy-prereq - 36. destroy-azure-azure634f9 - 37. destroy - 38. diagnostics - 39. publish-prereq - 40. publish-azure634f9 - 41. validate-appservice-config-env - 42. publish - 43. publish-manifest - 44. push + 9. validate-build-only-container-references + 10. deploy-prereq + 11. build-api + 12. build + 13. validate-azure-login + 14. create-provisioning-context + 15. provision-api-identity + 16. provision-cache-kv + 17. provision-api-roles-cache-kv + 18. provision-cosmos-kv + 19. provision-api-roles-cosmos-kv + 20. provision-pg-kv + 21. provision-api-roles-pg-kv + 22. provision-cache + 23. provision-cosmos + 24. provision-env-acr + 25. provision-env + 26. provision-pg + 27. login-to-acr-env-acr + 28. push-prereq + 29. push-api + 30. provision-api-website + 31. print-api-summary + 32. provision-azure-bicep-resources + 33. print-dashboard-url-env + 34. deploy + 35. deploy-api + 36. destroy-prereq + 37. destroy-azure-azure634f9 + 38. destroy + 39. diagnostics + 40. publish-prereq + 41. publish-azure634f9 + 42. validate-appservice-config-env + 43. publish + 44. publish-manifest + 45. push DETAILED STEP ANALYSIS ====================== @@ -108,7 +109,7 @@ Step: deploy-api Step: deploy-prereq Description: Prerequisite step that runs before any deploy operations. Initializes deployment environment and manages deployment state. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: destroy Description: Aggregation step for all destroy operations. All destroy steps should be required by this step. @@ -252,7 +253,7 @@ Step: publish-manifest Step: publish-prereq Description: Prerequisite step that runs before any publish operations. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: push Description: Aggregation step for all push operations. All push steps should be required by this step. @@ -280,6 +281,10 @@ Step: validate-azure-login Dependencies: ✓ deploy-prereq Resource: azure634f9 (AzureEnvironmentResource) +Step: validate-build-only-container-references + Description: Validates that build-only containers are consumed by another resource before publish or deploy. + Dependencies: none + Step: validate-compute-environments Description: Validates compute resource bindings before startup. Dependencies: none @@ -311,18 +316,18 @@ If targeting 'before-start': If targeting 'build': Direct dependencies: build-api - Total steps: 6 + Total steps: 7 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api [3] build If targeting 'build-api': Direct dependencies: build-prereq, check-container-runtime, deploy-prereq - Total steps: 5 + Total steps: 6 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api @@ -341,18 +346,18 @@ If targeting 'check-container-runtime': If targeting 'create-provisioning-context': Direct dependencies: deploy-prereq, validate-azure-login - Total steps: 4 + Total steps: 5 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context If targeting 'deploy': Direct dependencies: build-api, create-provisioning-context, print-api-summary, print-dashboard-url-env, provision-azure-bicep-resources, validate-azure-login - Total steps: 27 + Total steps: 28 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -367,9 +372,9 @@ If targeting 'deploy': If targeting 'deploy-api': Direct dependencies: print-api-summary - Total steps: 25 + Total steps: 26 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -382,10 +387,10 @@ If targeting 'deploy-api': [10] deploy-api If targeting 'deploy-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq If targeting 'destroy': @@ -417,9 +422,9 @@ If targeting 'diagnostics': If targeting 'login-to-acr-env-acr': Direct dependencies: provision-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -435,9 +440,9 @@ If targeting 'prepare-azure-app-service-env': If targeting 'print-api-summary': Direct dependencies: provision-api-website - Total steps: 24 + Total steps: 25 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -450,9 +455,9 @@ If targeting 'print-api-summary': If targeting 'print-dashboard-url-env': Direct dependencies: provision-azure-bicep-resources, provision-env - Total steps: 25 + Total steps: 26 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -472,9 +477,9 @@ If targeting 'process-parameters': If targeting 'provision-api-identity': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -482,9 +487,9 @@ If targeting 'provision-api-identity': If targeting 'provision-api-roles-cache-kv': Direct dependencies: create-provisioning-context, provision-api-identity, provision-cache-kv - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -493,9 +498,9 @@ If targeting 'provision-api-roles-cache-kv': If targeting 'provision-api-roles-cosmos-kv': Direct dependencies: create-provisioning-context, provision-api-identity, provision-cosmos-kv - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -504,9 +509,9 @@ If targeting 'provision-api-roles-cosmos-kv': If targeting 'provision-api-roles-pg-kv': Direct dependencies: create-provisioning-context, provision-api-identity, provision-pg-kv - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -515,9 +520,9 @@ If targeting 'provision-api-roles-pg-kv': If targeting 'provision-api-website': Direct dependencies: create-provisioning-context, provision-api-identity, provision-api-roles-cache-kv, provision-api-roles-cosmos-kv, provision-api-roles-pg-kv, provision-cache, provision-cache-kv, provision-cosmos, provision-cosmos-kv, provision-env, provision-pg, provision-pg-kv, push-api - Total steps: 23 + Total steps: 24 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -529,9 +534,9 @@ If targeting 'provision-api-website': If targeting 'provision-azure-bicep-resources': Direct dependencies: create-provisioning-context, deploy-prereq, provision-api-identity, provision-api-roles-cache-kv, provision-api-roles-cosmos-kv, provision-api-roles-pg-kv, provision-api-website, provision-cache, provision-cache-kv, provision-cosmos, provision-cosmos-kv, provision-env, provision-env-acr, provision-pg, provision-pg-kv - Total steps: 24 + Total steps: 25 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -544,9 +549,9 @@ If targeting 'provision-azure-bicep-resources': If targeting 'provision-cache': Direct dependencies: create-provisioning-context, provision-cache-kv - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -555,9 +560,9 @@ If targeting 'provision-cache': If targeting 'provision-cache-kv': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -565,9 +570,9 @@ If targeting 'provision-cache-kv': If targeting 'provision-cosmos': Direct dependencies: create-provisioning-context, provision-cosmos-kv - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -576,9 +581,9 @@ If targeting 'provision-cosmos': If targeting 'provision-cosmos-kv': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -586,9 +591,9 @@ If targeting 'provision-cosmos-kv': If targeting 'provision-env': Direct dependencies: create-provisioning-context, provision-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -597,9 +602,9 @@ If targeting 'provision-env': If targeting 'provision-env-acr': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -607,9 +612,9 @@ If targeting 'provision-env-acr': If targeting 'provision-pg': Direct dependencies: create-provisioning-context, provision-pg-kv - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -618,9 +623,9 @@ If targeting 'provision-pg': If targeting 'provision-pg-kv': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -628,18 +633,18 @@ If targeting 'provision-pg-kv': If targeting 'publish': Direct dependencies: publish-azure634f9, validate-appservice-config-env - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 | validate-appservice-config-env (parallel) [3] publish If targeting 'publish-azure634f9': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 @@ -650,17 +655,17 @@ If targeting 'publish-manifest': [0] publish-manifest If targeting 'publish-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq If targeting 'push': Direct dependencies: push-api, push-prereq - Total steps: 12 + Total steps: 13 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -672,9 +677,9 @@ If targeting 'push': If targeting 'push-api': Direct dependencies: build-api, push-prereq - Total steps: 11 + Total steps: 12 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -685,9 +690,9 @@ If targeting 'push-api': If targeting 'push-prereq': Direct dependencies: login-to-acr-env-acr - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -697,9 +702,9 @@ If targeting 'push-prereq': If targeting 'validate-appservice-config-env': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] validate-appservice-config-env @@ -711,12 +716,18 @@ If targeting 'validate-azure-app-service': If targeting 'validate-azure-login': Direct dependencies: deploy-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login +If targeting 'validate-build-only-container-references': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] validate-build-only-container-references + If targeting 'validate-compute-environments': Direct dependencies: none Total steps: 1 diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 96d373bcc0f..f91efadd206 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -10151,6 +10151,18 @@ func NewIDistributedApplicationPipeline(handle *Handle, client *AspireClient) *I } } +// DisableBuildOnlyContainerValidation disables publish and deploy validation for unconsumed build-only containers. +func (s *IDistributedApplicationPipeline) DisableBuildOnlyContainerValidation() (*IDistributedApplicationPipeline, error) { + reqArgs := map[string]any{ + "pipeline": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/disableBuildOnlyContainerValidation", reqArgs) + if err != nil { + return nil, err + } + return result.(*IDistributedApplicationPipeline), nil +} + // AddStep adds a pipeline step to the application func (s *IDistributedApplicationPipeline) AddStep(stepName string, callback func(...any) any, dependsOn []string, requiredBy []string) error { reqArgs := map[string]any{ diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index b27d5752d92..dc10ced1f7e 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -12001,6 +12001,13 @@ public class IDistributedApplicationPipeline extends HandleWrapperBase { super(handle, client); } + /** Disables publish and deploy validation for unconsumed build-only containers. */ + public IDistributedApplicationPipeline disableBuildOnlyContainerValidation() { + Map reqArgs = new HashMap<>(); + reqArgs.put("pipeline", AspireClient.serializeValue(getHandle())); + return (IDistributedApplicationPipeline) getClient().invokeCapability("Aspire.Hosting/disableBuildOnlyContainerValidation", reqArgs); + } + /** Adds a pipeline step to the application */ public void addStep(String stepName, AspireAction1 callback, AddStepOptions options) { var dependsOn = options == null ? null : options.getDependsOn(); diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index e0f5c405739..825d27fadba 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -2370,6 +2370,15 @@ def handle(self) -> Handle: """The underlying object reference handle.""" return self._handle + def disable_build_only_container_validation(self) -> AbstractDistributedApplicationPipeline: + """Disables publish and deploy validation for unconsumed build-only containers.""" + rpc_args: dict[str, typing.Any] = {'pipeline': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/disableBuildOnlyContainerValidation', + rpc_args, + ) + return typing.cast(AbstractDistributedApplicationPipeline, result) + def add_step(self, step_name: str, callback: typing.Callable[[PipelineStepContext], None], *, depends_on: typing.Iterable[str] | None = None, required_by: typing.Iterable[str] | None = None) -> None: """Adds a pipeline step to the application""" rpc_args: dict[str, typing.Any] = {'pipeline': self._handle} diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index e1f5035b52f..7691950d6ec 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -9223,6 +9223,15 @@ impl IDistributedApplicationPipeline { &self.client } + /// Disables publish and deploy validation for unconsumed build-only containers. + pub fn disable_build_only_container_validation(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("pipeline".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/disableBuildOnlyContainerValidation", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IDistributedApplicationPipeline::new(handle, self.client.clone())) + } + /// Adds a pipeline step to the application pub fn add_step(&self, step_name: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, depends_on: Option>, required_by: Option>) -> Result<(), Box> { let mut args: HashMap = HashMap::new(); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index fbfb93d7f97..5a3f8804061 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -6288,6 +6288,8 @@ class DistributedApplicationEventingPromiseImpl implements DistributedApplicatio export interface DistributedApplicationPipeline { toJSON(): MarshalledHandle; + /** Disables publish and deploy validation for unconsumed build-only containers. */ + disableBuildOnlyContainerValidation(): DistributedApplicationPipelinePromise; /** Adds a pipeline step to the application */ addStep(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: AddStepOptions): DistributedApplicationPipelinePromise; /** Configures the application pipeline via a callback */ @@ -6295,6 +6297,8 @@ export interface DistributedApplicationPipeline { } export interface DistributedApplicationPipelinePromise extends PromiseLike { + /** Disables publish and deploy validation for unconsumed build-only containers. */ + disableBuildOnlyContainerValidation(): DistributedApplicationPipelinePromise; /** Adds a pipeline step to the application */ addStep(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: AddStepOptions): DistributedApplicationPipelinePromise; /** Configures the application pipeline via a callback */ @@ -6314,6 +6318,20 @@ class DistributedApplicationPipelineImpl implements DistributedApplicationPipeli /** Serialize for JSON-RPC transport */ toJSON(): MarshalledHandle { return this._handle.toJSON(); } + /** @internal */ + async _disableBuildOnlyContainerValidationInternal(): Promise { + const rpcArgs: Record = { pipeline: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/disableBuildOnlyContainerValidation', + rpcArgs + ); + return new DistributedApplicationPipelineImpl(result, this._client); + } + + disableBuildOnlyContainerValidation(): DistributedApplicationPipelinePromise { + return new DistributedApplicationPipelinePromiseImpl(this._disableBuildOnlyContainerValidationInternal(), this._client); + } + /** @internal */ async _addStepInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[]): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -6373,6 +6391,10 @@ class DistributedApplicationPipelinePromiseImpl implements DistributedApplicatio return this._promise.then(onfulfilled, onrejected); } + disableBuildOnlyContainerValidation(): DistributedApplicationPipelinePromise { + return new DistributedApplicationPipelinePromiseImpl(this._promise.then(obj => obj.disableBuildOnlyContainerValidation()), this._client); + } + addStep(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: AddStepOptions): DistributedApplicationPipelinePromise { return new DistributedApplicationPipelinePromiseImpl(this._promise.then(obj => obj.addStep(stepName, callback, options)), this._client); } diff --git a/tests/Aspire.Hosting.Tests/BuildOnlyContainerValidationTests.cs b/tests/Aspire.Hosting.Tests/BuildOnlyContainerValidationTests.cs new file mode 100644 index 00000000000..f0e1a5dbbd6 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/BuildOnlyContainerValidationTests.cs @@ -0,0 +1,122 @@ +// 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 ASPIREPIPELINES001 + +using Aspire.Hosting.Pipelines; +using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Aspire.Hosting.Tests; + +[Trait("Partition", "6")] +public class BuildOnlyContainerValidationTests +{ + [Fact] + public async Task PublishPrereq_WithUnconsumedBuildOnlyContainer_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.PublishPrereq); + + var consumed = AddBuildOnlyContainer(builder, "frontend"); + AddBuildOnlyContainer(builder, "orphan"); + builder.AddResource(new TestContainerFilesDestinationResource("api")) + .PublishWithContainerFiles(consumed, "/app/wwwroot"); + + using var app = builder.Build(); + + var ex = await Assert.ThrowsAsync( + () => ExecutePipelineAsync(app)).DefaultTimeout(); + + Assert.Contains("'orphan'", ex.Message); + Assert.DoesNotContain("'frontend'", ex.Message); + Assert.Contains("PublishWithContainerFiles", ex.Message); + Assert.Contains("builder.Pipeline.DisableBuildOnlyContainerValidation", ex.Message); + } + + [Fact] + public async Task DeployPrereq_WithUnconsumedBuildOnlyContainer_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.DeployPrereq); + + var consumed = AddBuildOnlyContainer(builder, "frontend"); + AddBuildOnlyContainer(builder, "orphan"); + builder.AddResource(new TestContainerFilesDestinationResource("api")) + .PublishWithContainerFiles(consumed, "/app/wwwroot"); + + using var app = builder.Build(); + + var ex = await Assert.ThrowsAsync( + () => ExecutePipelineAsync(app)).DefaultTimeout(); + + Assert.Contains("'orphan'", ex.Message); + Assert.DoesNotContain("'frontend'", ex.Message); + } + + [Fact] + public async Task PublishPrereq_WithConsumedBuildOnlyContainer_DoesNotThrow() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.PublishPrereq); + + var source = AddBuildOnlyContainer(builder, "frontend"); + builder.AddResource(new TestContainerFilesDestinationResource("api")) + .PublishWithContainerFiles(source, "/app/wwwroot"); + + using var app = builder.Build(); + + await ExecutePipelineAsync(app).DefaultTimeout(); + } + + [Fact] + public async Task PublishPrereq_WithValidationDisabled_DoesNotThrow() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.PublishPrereq); + + PipelineStep? validationStep = null; + builder.Pipeline.DisableBuildOnlyContainerValidation(); + builder.Pipeline.AddPipelineConfiguration(context => + { + validationStep = context.Steps.Single(step => step.Name == DistributedApplicationPipeline.ValidateBuildOnlyContainerReferencesStepName); + return Task.CompletedTask; + }); + AddBuildOnlyContainer(builder, "frontend"); + + using var app = builder.Build(); + + await ExecutePipelineAsync(app).DefaultTimeout(); + + Assert.NotNull(validationStep); + Assert.Empty(validationStep.RequiredBySteps); + } + + private static IResourceBuilder AddBuildOnlyContainer( + IDistributedApplicationBuilder builder, + string name) + { + return builder.AddResource(new TestContainerFilesSourceResource(name)) + .WithAnnotation(new DockerfileBuildAnnotation("context", "Dockerfile", null) { HasEntrypoint = false }) + .WithAnnotation(new ContainerFilesSourceAnnotation { SourcePath = "/app/dist" }); + } + + private static Task ExecutePipelineAsync(DistributedApplication app) + { + var pipeline = app.Services.GetRequiredService(); + var context = new PipelineContext( + app.Services.GetRequiredService(), + app.Services.GetRequiredService(), + app.Services, + app.Services.GetRequiredService>(), + CancellationToken.None); + + return pipeline.ExecuteAsync(context); + } + + private sealed class TestContainerFilesSourceResource(string name) : ContainerResource(name), IResourceWithContainerFiles + { + } + + private sealed class TestContainerFilesDestinationResource(string name) : ContainerResource(name), IContainerFilesDestinationResource + { + } +} diff --git a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs index 68fbc01387e..cfca7ae5820 100644 --- a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs +++ b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs @@ -1558,7 +1558,7 @@ public async Task ExecuteAsync_WithConfigurationCallback_ExecutesCallback() await pipeline.ExecuteAsync(context).DefaultTimeout(); Assert.True(callbackExecuted); - Assert.Equal(17, capturedSteps.Count); // Default steps: deploy, deploy-prereq, process-parameters, build, build-prereq, check-container-runtime, push, push-prereq, publish, publish-prereq, diagnostics, validate-compute-environments, before-start, destroy, destroy-prereq + step1, step2 + Assert.Equal(18, capturedSteps.Count); // Default steps: deploy, deploy-prereq, process-parameters, build, build-prereq, check-container-runtime, push, push-prereq, publish, publish-prereq, validate-build-only-container-references, diagnostics, validate-compute-environments, before-start, destroy, destroy-prereq + step1, step2 Assert.Contains(capturedSteps, s => s.Name == "deploy"); Assert.Contains(capturedSteps, s => s.Name == "process-parameters"); Assert.Contains(capturedSteps, s => s.Name == "deploy-prereq"); @@ -1568,6 +1568,7 @@ public async Task ExecuteAsync_WithConfigurationCallback_ExecutesCallback() Assert.Contains(capturedSteps, s => s.Name == "push-prereq"); Assert.Contains(capturedSteps, s => s.Name == "publish"); Assert.Contains(capturedSteps, s => s.Name == "publish-prereq"); + Assert.Contains(capturedSteps, s => s.Name == "validate-build-only-container-references"); Assert.Contains(capturedSteps, s => s.Name == "diagnostics"); Assert.Contains(capturedSteps, s => s.Name == "validate-compute-environments"); Assert.Contains(capturedSteps, s => s.Name == "before-start"); From 81c6450dcc2d22929cbd359faf714efaadcd9fca Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:33:13 -0700 Subject: [PATCH 08/55] [release/13.3] Normalize CLI yes/no prompts (#16597) * Normalize CLI yes/no prompts Use single-key confirmation prompts for CLI yes/no choices so y/n answers are accepted without arrow-key selections while preserving [Y/n] and [y/N] defaults. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Prefer local hive versions in aspire add Treat the configured local hive as a local build channel so aspire add keeps generated AppHosts on the same CLI/SDK version and writes the local NuGet source when needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "Prefer local hive versions in aspire add" This reverts commit 682178778ffedf19ddbffc863b88a714b2658f05. --------- Co-authored-by: Sebastien Ros Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Interaction/ConsoleInteractionService.cs | 16 ++-- src/Aspire.Cli/Interaction/PromptBinding.cs | 63 ++++++++++++--- .../Packaging/NuGetConfigPrompter.cs | 14 +--- .../CliTemplateFactory.EmptyTemplate.cs | 8 +- ...liTemplateFactory.PythonStarterTemplate.cs | 8 +- .../Templating/DotNetTemplateFactory.cs | 23 +++--- .../Helpers/KubernetesDeployTestHelpers.cs | 3 +- .../Commands/NewCommandTests.cs | 22 +++-- .../ConsoleInteractionServiceTests.cs | 40 ++++++++- tests/Shared/Hex1bAutomatorTestHelpers.cs | 81 +++++++++++++------ tests/Shared/Hex1bTestHelpers.cs | 9 ++- 11 files changed, 188 insertions(+), 99 deletions(-) diff --git a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs index 872158d28a9..4ddd2476f79 100644 --- a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs @@ -161,10 +161,10 @@ public async Task PromptForStringAsync(string promptText, Func PromptForSelectionAsync(string promptText, IEnumerable> PromptForSelectionsAsync(string promptTex { if (binding != null) { - if (defaultValue != null) + if (binding.NonInteractiveDefaultValue != null) { - return MatchChoicesOrThrow(defaultValue, binding, choicesList, choiceFormatter); + return MatchChoicesOrThrow(binding.NonInteractiveDefaultValue, binding, choicesList, choiceFormatter); } ThrowNonInteractiveError(binding.SymbolDisplayName); @@ -499,7 +499,7 @@ public async Task PromptConfirmAsync(string promptText, PromptBinding resolver, T? defaultValue, bool hasExplicitDefault) + : this(parseResult, symbolDisplayName, resolver, defaultValue, hasExplicitDefault, hasExplicitDefault ? defaultValue : default) + { + } + + internal PromptBinding( + ParseResult? parseResult, + string symbolDisplayName, + Func resolver, + T? defaultValue, + bool hasExplicitDefault, + T? nonInteractiveDefaultValue) { _parseResult = parseResult; SymbolDisplayName = symbolDisplayName; _resolver = resolver; DefaultValue = defaultValue; HasExplicitDefault = hasExplicitDefault; + NonInteractiveDefaultValue = nonInteractiveDefaultValue; } /// @@ -38,10 +49,15 @@ internal PromptBinding( public string SymbolDisplayName { get; } /// - /// Gets the default value to use when non-interactive and the symbol was not provided. + /// Gets the default value to use for interactive prompts when the symbol was not provided. /// public T? DefaultValue { get; } + /// + /// Gets the default value to use when non-interactive and the symbol was not provided. + /// + public T? NonInteractiveDefaultValue { get; } + /// /// Gets whether a default value was explicitly specified when this binding was created. /// When false, prompt methods should throw in non-interactive mode instead of @@ -67,7 +83,7 @@ internal PromptBinding( /// Creates a new with the same resolver but a different default value. /// public PromptBinding WithDefault(T? newDefault) => - new(_parseResult, SymbolDisplayName, _resolver, newDefault, hasExplicitDefault: true); + new(_parseResult, SymbolDisplayName, _resolver, newDefault, hasExplicitDefault: true, nonInteractiveDefaultValue: newDefault); } /// @@ -115,16 +131,39 @@ public static PromptBinding CreateInvertedBoolConfirm(ParseResult parseRes new(parseResult, FormatOptionName(option), BuildResolver(option, value => value != true), defaultValue, hasExplicitDefault: true); /// - /// Creates a for a bool? option that maps to Yes/No selection choices. - /// When the option is explicitly provided, resolves to or . - /// When not provided in non-interactive mode, defaults to . + /// Creates a for a bool? option that maps to a confirmation prompt. + /// When the option is explicitly provided, the binding resolves to true only when the option value is true. + /// When the option is not explicitly provided, is used as the confirmation default, + /// including as the interactive prompt default when the user accepts the prompt by pressing Enter. /// - public static PromptBinding CreateBoolAsSelection(ParseResult parseResult, Option option, string? trueValue = null, string? falseValue = null) - { - trueValue ??= TemplatingStrings.Yes; - falseValue ??= TemplatingStrings.No; - return new(parseResult, FormatOptionName(option), BuildResolver(option, value => value == true ? trueValue : falseValue), falseValue, hasExplicitDefault: true); - } + /// The parse result used to determine whether was explicitly provided. + /// The nullable Boolean option to bind to the confirmation prompt. + /// The default confirmation value to use when was not explicitly provided. + /// + /// A that resolves the explicitly provided bool? option to a , + /// where true maps to true and any other value maps to false, and otherwise exposes + /// as the prompt default. + /// + public static PromptBinding CreateBoolConfirm(ParseResult parseResult, Option option, bool defaultValue) => + CreateBoolConfirm(parseResult, option, interactiveDefault: defaultValue, nonInteractiveDefault: defaultValue); + + /// + /// Creates a for a bool? option that maps to a confirmation prompt. + /// When the option is explicitly provided, the binding resolves to true only when the option value is true. + /// When the option is not explicitly provided, is used as the confirmation prompt default, + /// and is used when interactive input is not available. + /// + /// The parse result used to determine whether was explicitly provided. + /// The nullable Boolean option to bind to the confirmation prompt. + /// The default confirmation value to use for the interactive prompt. + /// The default confirmation value to use when interactive input is not available. + /// + /// A that resolves the explicitly provided bool? option to a , + /// where true maps to true and any other value maps to false, and otherwise exposes + /// as the prompt default. + /// + public static PromptBinding CreateBoolConfirm(ParseResult parseResult, Option option, bool interactiveDefault, bool nonInteractiveDefault) => + new(parseResult, FormatOptionName(option), BuildResolver(option, value => value == true), interactiveDefault, hasExplicitDefault: true, nonInteractiveDefaultValue: nonInteractiveDefault); private static string FormatOptionName(Option option) => $"'{option.Name}'"; diff --git a/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs b/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs index 3a1d0569d32..f03a63de05d 100644 --- a/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs +++ b/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs @@ -47,13 +47,10 @@ public async Task PromptToCreateOrUpdateAsync(DirectoryInfo targetDirectory, Pac if (!hasConfigInTargetDir) { - var choice = await _interactionService.PromptForSelectionAsync( + var shouldCreate = await _interactionService.PromptConfirmAsync( TemplatingStrings.CreateNugetConfigConfirmation, - [TemplatingStrings.Yes, TemplatingStrings.No], - c => c, - binding: PromptBinding.CreateDefault(TemplatingStrings.Yes), + binding: PromptBinding.CreateDefault(true), cancellationToken: cancellationToken); - var shouldCreate = string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput); if (shouldCreate) { @@ -63,13 +60,10 @@ public async Task PromptToCreateOrUpdateAsync(DirectoryInfo targetDirectory, Pac } else if (hasMissingSources) { - var updateChoice = await _interactionService.PromptForSelectionAsync( + var shouldUpdate = await _interactionService.PromptConfirmAsync( TemplatingStrings.UpdateNuGetConfigConfirmation, - [TemplatingStrings.Yes, TemplatingStrings.No], - c => c, - binding: PromptBinding.CreateDefault(TemplatingStrings.Yes), + binding: PromptBinding.CreateDefault(true), cancellationToken: cancellationToken); - var shouldUpdate = string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput); if (shouldUpdate) { diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs index 020287e3058..9b929aa3430 100644 --- a/src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs +++ b/src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs @@ -108,17 +108,13 @@ private async Task ApplyEmptyAppHostTemplateAsync(CallbackTempla private async Task ResolveUseLocalhostTldAsync(System.CommandLine.ParseResult parseResult, CancellationToken cancellationToken) { - var binding = PromptBinding.CreateBoolAsSelection(parseResult, _localhostTldOption); + var binding = PromptBinding.CreateBoolConfirm(parseResult, _localhostTldOption, defaultValue: false); - var selected = await _interactionService.PromptForSelectionAsync( + var useLocalhostTld = await _interactionService.PromptConfirmAsync( TemplatingStrings.UseLocalhostTld_Prompt, - [TemplatingStrings.No, TemplatingStrings.Yes], - choice => choice, binding: binding, cancellationToken: cancellationToken); - var useLocalhostTld = string.Equals(selected, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput); - if (useLocalhostTld) { _interactionService.DisplayMessage(KnownEmojis.CheckMarkButton, TemplatingStrings.UseLocalhostTld_UsingLocalhostTld); diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs index f877d320893..5a386d1734f 100644 --- a/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs +++ b/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs @@ -121,17 +121,13 @@ string ApplyAllTokens(string content) => ConditionalBlockProcessor.Process( private async Task ResolveUseRedisCacheAsync(System.CommandLine.ParseResult parseResult, CancellationToken cancellationToken) { - var binding = PromptBinding.CreateBoolAsSelection(parseResult, _useRedisCacheOption); + var binding = PromptBinding.CreateBoolConfirm(parseResult, _useRedisCacheOption, interactiveDefault: true, nonInteractiveDefault: false); - var selected = await _interactionService.PromptForSelectionAsync( + var useRedisCache = await _interactionService.PromptConfirmAsync( TemplatingStrings.UseRedisCache_Prompt, - [TemplatingStrings.Yes, TemplatingStrings.No], - choice => choice, binding: binding, cancellationToken: cancellationToken); - var useRedisCache = string.Equals(selected, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput); - if (useRedisCache) { _interactionService.DisplayMessage(KnownEmojis.CheckMarkButton, TemplatingStrings.UseRedisCache_UsingRedisCache); diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs index 5b8f2166ffd..9941bdcb5ac 100644 --- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs +++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs @@ -290,16 +290,14 @@ private async Task PromptForExtraAspireXUnitOptionsAsync(ParseResult r private async Task PromptForDevLocalhostTldOptionAsync(ParseResult result, List extraArgs, CancellationToken cancellationToken) { - var binding = PromptBinding.CreateBoolAsSelection(result, _localhostTldOption); + var binding = PromptBinding.CreateBoolConfirm(result, _localhostTldOption, defaultValue: false); - var selected = await interactionService.PromptForSelectionAsync( + var useLocalhostTld = await interactionService.PromptConfirmAsync( TemplatingStrings.UseLocalhostTld_Prompt, - [TemplatingStrings.No, TemplatingStrings.Yes], - choice => choice, binding: binding, cancellationToken: cancellationToken); - if (string.Equals(selected, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) + if (useLocalhostTld) { interactionService.DisplayMessage(KnownEmojis.CheckMarkButton, TemplatingStrings.UseLocalhostTld_UsingLocalhostTld); extraArgs.Add("--localhost-tld"); @@ -308,16 +306,14 @@ private async Task PromptForDevLocalhostTldOptionAsync(ParseResult result, List< private async Task PromptForRedisCacheOptionAsync(ParseResult result, List extraArgs, CancellationToken cancellationToken) { - var binding = PromptBinding.CreateBoolAsSelection(result, _useRedisCacheOption); + var binding = PromptBinding.CreateBoolConfirm(result, _useRedisCacheOption, interactiveDefault: true, nonInteractiveDefault: false); - var selected = await interactionService.PromptForSelectionAsync( + var useRedisCache = await interactionService.PromptConfirmAsync( TemplatingStrings.UseRedisCache_Prompt, - [TemplatingStrings.Yes, TemplatingStrings.No], - choice => choice, binding: binding, cancellationToken: cancellationToken); - if (string.Equals(selected, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) + if (useRedisCache) { interactionService.DisplayMessage(KnownEmojis.CheckMarkButton, TemplatingStrings.UseRedisCache_UsingRedisCache); extraArgs.Add("--use-redis-cache"); @@ -336,13 +332,12 @@ private async Task PromptForTestFrameworkOptionsAsync(ParseResult result, List choice, + binding: PromptBinding.CreateDefault(false), cancellationToken: cancellationToken); - if (string.Equals(createTestProject, TemplatingStrings.No, StringComparisons.CliInputOrOutput)) + if (!createTestProject) { return; } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/KubernetesDeployTestHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/KubernetesDeployTestHelpers.cs index 43d1fc325c7..33773f0bb9f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/KubernetesDeployTestHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/KubernetesDeployTestHelpers.cs @@ -187,8 +187,7 @@ await auto.WaitUntilAsync( s => new CellPatternSearcher().Find("Use Redis Cache").Search(s).Count > 0, timeout: TimeSpan.FromSeconds(10), description: "Redis cache prompt"); - await auto.DownAsync(); // Navigate to "No" - await auto.EnterAsync(); + await auto.TypeAsync("n"); await auto.WaitUntilAsync( s => new CellPatternSearcher().Find("Do you want to create a test project?").Search(s).Count > 0, diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index 45cd3da2c46..c877d3af328 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -913,7 +913,7 @@ public async Task NewCommandWithExplicitCSharpEmptyTemplateCreatesCSharpAppHost( } [Fact] - public async Task NewCommandWithEmptyTemplateAndCSharpPromptsForLocalhostTldAndUsesSelection() + public async Task NewCommandWithEmptyTemplateAndCSharpPromptsForLocalhostTldAndUsesConfirmation() { using var workspace = TemporaryWorkspace.Create(outputHelper); var localhostPrompted = false; @@ -924,17 +924,16 @@ public async Task NewCommandWithEmptyTemplateAndCSharpPromptsForLocalhostTldAndU options.InteractionServiceFactory = _ => new TestInteractionService { - ConfirmCallback = (_, _) => false, - PromptForSelectionCallback = (promptText, choices, choiceFormatter, cancellationToken) => + ConfirmCallback = (promptText, defaultValue) => { if (string.Equals(promptText, TemplatingStrings.UseLocalhostTld_Prompt, StringComparison.Ordinal)) { localhostPrompted = true; - return choices.Cast().Single(choice => - string.Equals(choiceFormatter(choice), TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)); + Assert.False(defaultValue); + return true; } - return choices.Cast().First(); + return false; } }; options.NewCommandPrompterFactory = (sp) => @@ -1037,7 +1036,7 @@ public async Task NewCommandWithEmptyTemplateNormalizesDefaultOutputPath() } [Fact] - public async Task NewCommandWithEmptyTemplateAndTypeScriptPromptsForLocalhostTldAndUsesSelection() + public async Task NewCommandWithEmptyTemplateAndTypeScriptPromptsForLocalhostTldAndUsesConfirmation() { using var workspace = TemporaryWorkspace.Create(outputHelper); var scaffoldingInvoked = false; @@ -1049,17 +1048,16 @@ public async Task NewCommandWithEmptyTemplateAndTypeScriptPromptsForLocalhostTld options.InteractionServiceFactory = _ => new TestInteractionService { - ConfirmCallback = (_, _) => false, - PromptForSelectionCallback = (promptText, choices, choiceFormatter, cancellationToken) => + ConfirmCallback = (promptText, defaultValue) => { if (string.Equals(promptText, TemplatingStrings.UseLocalhostTld_Prompt, StringComparison.Ordinal)) { localhostPrompted = true; - return choices.Cast().Single(choice => - string.Equals(choiceFormatter(choice), TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)); + Assert.False(defaultValue); + return true; } - return choices.Cast().First(); + return false; } }; options.NewCommandPrompterFactory = (sp) => diff --git a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs index da988bee0b7..da1e3b6c7c3 100644 --- a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs @@ -1008,6 +1008,41 @@ public async Task ConfirmAsync_NonInteractive_WithExplicitDefault_ReturnsDefault Assert.True(result); } + [Fact] + public async Task ConfirmAsync_NonInteractive_WithSeparateNonInteractiveDefault_ReturnsNonInteractiveDefault() + { + var output = new StringBuilder(); + var console = CreateInteractiveConsoleWithInput(output, ""); + var interactionService = CreateInteractionService(console, hostEnvironment: TestHelpers.CreateNonInteractiveHostEnvironment()); + + var option = new System.CommandLine.Option("--confirm"); + var command = new System.CommandLine.RootCommand { option }; + var parseResult = command.Parse(""); + var binding = PromptBinding.CreateBoolConfirm(parseResult, option, interactiveDefault: true, nonInteractiveDefault: false); + + var result = await interactionService.PromptConfirmAsync("Proceed?", binding: binding, cancellationToken: CancellationToken.None); + + Assert.False(result); + } + + [Fact] + public async Task ConfirmAsync_Interactive_WithSeparateNonInteractiveDefault_UsesInteractiveDefault() + { + var output = new StringBuilder(); + var console = CreateInteractiveConsoleWithInput(output, "\n"); + var interactionService = CreateInteractionService(console); + + var option = new System.CommandLine.Option("--confirm"); + var command = new System.CommandLine.RootCommand { option }; + var parseResult = command.Parse(""); + var binding = PromptBinding.CreateBoolConfirm(parseResult, option, interactiveDefault: true, nonInteractiveDefault: false); + + var result = await interactionService.PromptConfirmAsync("Proceed?", binding: binding, cancellationToken: CancellationToken.None); + + Assert.True(result); + Assert.Contains("[Y/n]", output.ToString()); + } + [Fact] public void MatchChoices_WithDuplicateValues_ReturnsDeduplicated() { @@ -1242,13 +1277,13 @@ public void PromptBinding_InvertedBoolConfirm_SymbolDisplayName_IsCorrect() } [Fact] - public void PromptBinding_BoolAsSelection_SymbolDisplayName_IsCorrect() + public void PromptBinding_BoolConfirm_SymbolDisplayName_IsCorrect() { var option = new System.CommandLine.Option("--include"); var command = new System.CommandLine.RootCommand { option }; var parseResult = command.Parse("--include"); - var binding = PromptBinding.CreateBoolAsSelection(parseResult, option, "Yes", "No"); + var binding = PromptBinding.CreateBoolConfirm(parseResult, option, defaultValue: false); Assert.Equal("'--include'", binding.SymbolDisplayName); } @@ -1260,6 +1295,7 @@ public void PromptBinding_WithDefault_ChangesDefault() var updated = binding.WithDefault("new-value"); Assert.Equal("new-value", updated.DefaultValue); + Assert.Equal("new-value", updated.NonInteractiveDefaultValue); Assert.True(updated.HasExplicitDefault); } diff --git a/tests/Shared/Hex1bAutomatorTestHelpers.cs b/tests/Shared/Hex1bAutomatorTestHelpers.cs index 0773662f788..ece616d1e01 100644 --- a/tests/Shared/Hex1bAutomatorTestHelpers.cs +++ b/tests/Shared/Hex1bAutomatorTestHelpers.cs @@ -619,10 +619,12 @@ await auto.WaitUntilAsync( if (!useRedisCache) { - await auto.DownAsync(); // Default is "Yes", navigate to "No" + await auto.TypeAsync("n"); + } + else + { + await auto.EnterAsync(); } - - await auto.EnterAsync(); } // Step 7: Test project prompt (only Starter) @@ -655,32 +657,65 @@ internal static async Task AspireInitAsync( var waitingForInitComplete = new CellPatternSearcher() .Find("Aspire initialization complete"); + var waitingForAgentInitPrompt = new CellPatternSearcher() + .Find("configure AI agent environments"); + await auto.TypeAsync("aspire init --language csharp"); await auto.EnterAsync(); - // NuGet.config prompt may or may not appear depending on environment. - // Wait for either the NuGet.config prompt or the URLs prompt. - await auto.WaitUntilAsync( - s => waitingForNuGetConfigPrompt.Search(s).Count > 0 - || waitingForUrlsPrompt.Search(s).Count > 0, - timeout: TimeSpan.FromMinutes(2), - description: "NuGet.config prompt or URLs prompt"); - await auto.EnterAsync(); // Dismiss NuGet.config prompt if present + var handledNuGetConfigPrompt = false; + var handledUrlsPrompt = false; - // Wait for the URLs prompt (if NuGet.config appeared first) or init completion. - await auto.WaitUntilAsync( - s => waitingForUrlsPrompt.Search(s).Count > 0 - || waitingForInitComplete.Search(s).Count > 0, - timeout: TimeSpan.FromMinutes(2), - description: "URLs prompt or init completion"); - await auto.EnterAsync(); // Dismiss URLs prompt (accept default "No") + while (true) + { + var initState = "unknown"; + await auto.WaitUntilAsync(s => + { + if (!handledNuGetConfigPrompt && waitingForNuGetConfigPrompt.Search(s).Count > 0) + { + initState = "nuget-config"; + return true; + } - await auto.WaitUntilAsync( - s => waitingForInitComplete.Search(s).Count > 0, - timeout: TimeSpan.FromMinutes(2), - description: "aspire initialization complete"); + if (!handledUrlsPrompt && waitingForUrlsPrompt.Search(s).Count > 0) + { + initState = "urls"; + return true; + } - await auto.DeclineAgentInitPromptAsync(counter); + if (waitingForAgentInitPrompt.Search(s).Count > 0) + { + initState = "agent-init"; + return true; + } + + if (waitingForInitComplete.Search(s).Count > 0) + { + initState = "init-complete"; + return true; + } + + return false; + }, timeout: TimeSpan.FromMinutes(2), description: "NuGet.config prompt, URLs prompt, agent init prompt, or init completion"); + + if (initState is "nuget-config" or "urls") + { + if (initState == "nuget-config") + { + handledNuGetConfigPrompt = true; + } + else + { + handledUrlsPrompt = true; + } + + await auto.EnterAsync(); + continue; + } + + await auto.DeclineAgentInitPromptAsync(counter); + return; + } } /// diff --git a/tests/Shared/Hex1bTestHelpers.cs b/tests/Shared/Hex1bTestHelpers.cs index 81967e6a272..c30a5e02a35 100644 --- a/tests/Shared/Hex1bTestHelpers.cs +++ b/tests/Shared/Hex1bTestHelpers.cs @@ -494,11 +494,12 @@ internal static Hex1bTerminalInputSequenceBuilder AspireNew( if (!useRedisCache) { - // Default is "Yes", navigate to "No" - builder.Key(Hex1bKey.DownArrow); + builder.Type("n"); + } + else + { + builder.Enter(); } - - builder.Enter(); } // Step 7: Test project prompt (only Starter) From 5b64970763bd1d1a3b66bb06731020d3c41b4086 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:31:23 -0700 Subject: [PATCH 09/55] [release/13.3] Publish native Aspire CLI tool packages (#16611) * fix(publishing): stage native CLI tool packages Download RID-specific Aspire.Cli tool packages from native build artifacts and stage them in the shipping packages directory so publishing can include them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(publishing): validate CLI package RIDs Compare discovered CLI archives and RID-specific tool packages against the expected clipack RIDs, require exactly one pointer package, and summarize publish output without listing every NuGet package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ankit Jain Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Publishing.props | 72 ++++++++++++++++---- eng/pipelines/azure-pipelines-unofficial.yml | 30 ++++++++ eng/pipelines/azure-pipelines.yml | 30 ++++++++ 3 files changed, 119 insertions(+), 13 deletions(-) diff --git a/eng/Publishing.props b/eng/Publishing.props index a5b86c8b8da..9b46fc8f629 100644 --- a/eng/Publishing.props +++ b/eng/Publishing.props @@ -34,15 +34,12 @@ - <_ArchiveFiles Include="$(ArtifactsPackagesDir)\**\aspire-cli-*.zip" /> - <_ArchiveFiles Include="$(ArtifactsPackagesDir)\**\aspire-cli-*.tar.gz" /> - - <_ArchiveFiles Include="$(_SignedArchivesDir)\**\aspire-cli-*.zip" /> - <_ArchiveFiles Include="$(_SignedArchivesDir)\**\aspire-cli-*.tar.gz" /> <_ExtensionFilesToPublish Include="$(ArtifactsPackagesDir)**\aspire-vscode-*.vsix" /> <_ExtensionManifestFiles Include="$(ArtifactsPackagesDir)**\aspire-vscode-*.manifest" /> <_ExtensionSignatureFiles Include="$(ArtifactsPackagesDir)**\aspire-vscode-*.signature.p7s" /> + <_CliToolPackagesToPublish Include="$(ArtifactsShippingPackagesDir)**\Aspire.Cli*.nupkg" + Exclude="$(ArtifactsShippingPackagesDir)**\*.symbols.nupkg" /> + <_CliPackProjects Include="$(RepoRoot)eng\clipack\Aspire.Cli.*.csproj" /> + <_ExpectedCliRids Include="@(_CliPackProjects->'%(Filename)'->Replace('Aspire.Cli.', ''))" /> + + + + <_ArchiveFiles Include="$(ArtifactsPackagesDir)\**\aspire-cli-*.zip" /> + <_ArchiveFiles Include="$(ArtifactsPackagesDir)\**\aspire-cli-*.tar.gz" /> + + <_ArchiveFiles Include="$(_SignedArchivesDir)\**\aspire-cli-*.zip" /> + <_ArchiveFiles Include="$(_SignedArchivesDir)\**\aspire-cli-*.tar.gz" /> + <_ArchiveFilesWithRid Include="@(_ArchiveFiles)"> $([System.Text.RegularExpressions.Regex]::Match(%(_ArchiveFiles.FileName), 'aspire-cli-(.*)-\d+.*').Groups[1].Value) - <_CliPackProjects Include="$(RepoRoot)eng\clipack\Aspire.Cli.*.csproj" /> - <_ExpectedRids Include="@(_CliPackProjects->'%(Filename)'->Replace('Aspire.Cli.', ''))" /> + <_MissingArchiveRids Include="@(_ExpectedCliRids)" Exclude="@(_ArchiveFilesWithRid -> '%(ExtractedRid)')" /> + <_UnexpectedArchiveRids Include="@(_ArchiveFilesWithRid -> '%(ExtractedRid)')" Exclude="@(_ExpectedCliRids)" /> + + + + <_CliRidSpecificToolPackageFiles Include="@(_CliToolPackagesToPublish)" + Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch('%(_CliToolPackagesToPublish.FileName)', '^Aspire\.Cli\.\d'))" /> - <_MissingRids Include="@(_ExpectedRids)" Exclude="@(_ArchiveFilesWithRid -> '%(ExtractedRid)')" /> - <_UnexpectedRids Include="@(_ArchiveFilesWithRid -> '%(ExtractedRid)')" Exclude="@(_ExpectedRids)" /> + + <_CliRidSpecificToolPackageFilesWithRid Include="@(_CliRidSpecificToolPackageFiles)"> + + $([System.Text.RegularExpressions.Regex]::Match('%(_CliRidSpecificToolPackageFiles.FileName)', '^Aspire\.Cli\.(.*?)\.\d+.*').Groups[1].Value) + + + <_MissingCliPackageRids Include="@(_ExpectedCliRids)" Exclude="@(_CliRidSpecificToolPackageFilesWithRid -> '%(ExtractedRid)')" /> + <_UnexpectedCliPackageRids Include="@(_CliRidSpecificToolPackageFilesWithRid -> '%(ExtractedRid)')" Exclude="@(_ExpectedCliRids)" /> + <_CliPointerToolPackages Include="@(_CliToolPackagesToPublish)" Exclude="@(_CliRidSpecificToolPackageFilesWithRid)" /> - - + + + + + @@ -108,7 +132,7 @@ - + <_ExtensionFilesToPublish Remove="@(_ExtensionFilesToPublish)" /> @@ -149,6 +173,10 @@ true $(_UploadPathRoot)/$(_PackageVersion)/%(Filename)%(Extension) + + true + Package + false true @@ -168,5 +196,23 @@ $(_UploadPathRoot)/$(_PackageVersion)/%(Filename)%(Extension) + + + + + + <_PackageItemsToPush Include="@(ItemsToPushToBlobFeed)" Condition="'%(ItemsToPushToBlobFeed.Kind)' == 'Package'" /> + <_ShippingPackageItemsToPush Include="@(_PackageItemsToPush)" Condition="'%(_PackageItemsToPush.IsShipping)' == 'true'" /> + <_FlatBlobItemsToPush Include="@(ItemsToPushToBlobFeed)" Condition="'%(ItemsToPushToBlobFeed.Kind)' != 'Package' And '%(ItemsToPushToBlobFeed.PublishFlatContainer)' == 'true'" /> + <_ShippingFlatBlobItemsToPush Include="@(_FlatBlobItemsToPush)" Condition="'%(_FlatBlobItemsToPush.IsShipping)' == 'true'" /> + <_NonShippingFlatBlobItemsToPush Include="@(_FlatBlobItemsToPush)" Condition="'%(_FlatBlobItemsToPush.IsShipping)' != 'true'" /> + + + + + + + diff --git a/eng/pipelines/azure-pipelines-unofficial.yml b/eng/pipelines/azure-pipelines-unofficial.yml index dcd0c7ce260..910bd23d3bf 100644 --- a/eng/pipelines/azure-pipelines-unofficial.yml +++ b/eng/pipelines/azure-pipelines-unofficial.yml @@ -172,6 +172,36 @@ extends: # validation (Homebrew cask audit, WinGet manifest) against published URLs. targetPath: '$(Build.SourcesDirectory)/artifacts/signed-archives/$(_BuildConfig)' + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download Native CLI Tool Packages + inputs: + itemPattern: | + **/Aspire.Cli*.nupkg + targetPath: '$(Build.SourcesDirectory)/artifacts/native-cli-packages/$(_BuildConfig)' + + - pwsh: | + $ErrorActionPreference = 'Stop' + $downloadRoot = "$(Build.SourcesDirectory)/artifacts/native-cli-packages/$(_BuildConfig)" + $shippingDir = "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)/Shipping" + New-Item -ItemType Directory -Path $shippingDir -Force | Out-Null + + $packages = @(Get-ChildItem -Path $downloadRoot -Filter "Aspire.Cli*.nupkg" -File -Recurse | + Where-Object { $_.Name -notmatch '\.symbols\.' } | + Sort-Object FullName) + if ($packages.Count -eq 0) { + throw "No native CLI tool packages were downloaded to $downloadRoot." + } + + foreach ($packageGroup in $packages | Group-Object Name) { + $package = $packageGroup.Group[0] + if ($packageGroup.Count -gt 1) { + Write-Host "Using $($package.FullName) for duplicate package name $($packageGroup.Name)." + } + + Copy-Item -LiteralPath $package.FullName -Destination (Join-Path $shippingDir $package.Name) -Force + } + displayName: 🟣Stage Native CLI Tool Packages + - task: PowerShell@2 displayName: 🟣List artifacts packages contents inputs: diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml index 543441fd527..ad0824a1d13 100644 --- a/eng/pipelines/azure-pipelines.yml +++ b/eng/pipelines/azure-pipelines.yml @@ -260,6 +260,36 @@ extends: # validation (Homebrew cask audit, WinGet manifest) against published URLs. targetPath: '$(Build.SourcesDirectory)/artifacts/signed-archives/$(_BuildConfig)' + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download Native CLI Tool Packages + inputs: + itemPattern: | + **/Aspire.Cli*.nupkg + targetPath: '$(Build.SourcesDirectory)/artifacts/native-cli-packages/$(_BuildConfig)' + + - pwsh: | + $ErrorActionPreference = 'Stop' + $downloadRoot = "$(Build.SourcesDirectory)/artifacts/native-cli-packages/$(_BuildConfig)" + $shippingDir = "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)/Shipping" + New-Item -ItemType Directory -Path $shippingDir -Force | Out-Null + + $packages = @(Get-ChildItem -Path $downloadRoot -Filter "Aspire.Cli*.nupkg" -File -Recurse | + Where-Object { $_.Name -notmatch '\.symbols\.' } | + Sort-Object FullName) + if ($packages.Count -eq 0) { + throw "No native CLI tool packages were downloaded to $downloadRoot." + } + + foreach ($packageGroup in $packages | Group-Object Name) { + $package = $packageGroup.Group[0] + if ($packageGroup.Count -gt 1) { + Write-Host "Using $($package.FullName) for duplicate package name $($packageGroup.Name)." + } + + Copy-Item -LiteralPath $package.FullName -Destination (Join-Path $shippingDir $package.Name) -Force + } + displayName: 🟣Stage Native CLI Tool Packages + - task: PowerShell@2 displayName: 🟣List artifacts packages contents inputs: From 7458985c5bf5e97936e7addfd04911ee8cd07f0e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 1 May 2026 06:48:56 +0800 Subject: [PATCH 10/55] [release/13.3] Rename pipeline --log-level to --pipeline-log-level to avoid CLI log overlap (#16596) * Rename pipeline --log-level to --pipeline-log-level to avoid CLI log overlap The pipeline commands (do, publish, deploy, destroy) had a --log-level option that collided with the global CLI --log-level/-l option. When a user passed --log-level Debug to control pipeline verbosity, Program.ParseLoggingOptions() also picked it up and cranked CLI internal logging to Debug, flooding output with mixed noise. Rename the pipeline-specific option to --pipeline-log-level so the two concerns are cleanly separated: - --log-level / -l (global, recursive) controls CLI internal logging - --pipeline-log-level (pipeline commands only) controls pipeline step output The value is still forwarded as --log-level to the AppHost process, which is correct since the AppHost has its own --log-level parameter. Also adds tests verifying the separation and argument forwarding. * Rename s_logLevelOption to s_pipelineLogLevelOption --- src/Aspire.Cli/Commands/DeployCommand.cs | 2 +- src/Aspire.Cli/Commands/DestroyCommand.cs | 2 +- src/Aspire.Cli/Commands/DoCommand.cs | 2 +- .../Commands/PipelineCommandBase.cs | 6 +- src/Aspire.Cli/Commands/PublishCommand.cs | 2 +- .../ConsoleActivityLoggerStrings.resx | 2 +- .../xlf/ConsoleActivityLoggerStrings.cs.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.de.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.es.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.fr.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.it.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.ja.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.ko.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.pl.xlf | 4 +- .../ConsoleActivityLoggerStrings.pt-BR.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.ru.xlf | 4 +- .../xlf/ConsoleActivityLoggerStrings.tr.xlf | 4 +- .../ConsoleActivityLoggerStrings.zh-Hans.xlf | 4 +- .../ConsoleActivityLoggerStrings.zh-Hant.xlf | 4 +- .../Commands/DoCommandTests.cs | 107 ++++++++++++++++++ 20 files changed, 141 insertions(+), 34 deletions(-) diff --git a/src/Aspire.Cli/Commands/DeployCommand.cs b/src/Aspire.Cli/Commands/DeployCommand.cs index 9f483bd0eb4..d5b23482240 100644 --- a/src/Aspire.Cli/Commands/DeployCommand.cs +++ b/src/Aspire.Cli/Commands/DeployCommand.cs @@ -51,7 +51,7 @@ protected override Task GetRunArgumentsAsync(string? fullyQualifiedOut } // Add --log-level and --envionment flags if specified - var logLevel = parseResult.GetValue(s_logLevelOption); + var logLevel = parseResult.GetValue(s_pipelineLogLevelOption); if (!string.IsNullOrEmpty(logLevel)) { diff --git a/src/Aspire.Cli/Commands/DestroyCommand.cs b/src/Aspire.Cli/Commands/DestroyCommand.cs index 76d76b3d6f1..8169a4315a1 100644 --- a/src/Aspire.Cli/Commands/DestroyCommand.cs +++ b/src/Aspire.Cli/Commands/DestroyCommand.cs @@ -50,7 +50,7 @@ protected override Task GetRunArgumentsAsync(string? fullyQualifiedOut baseArgs.AddRange(["--yes", "true"]); } - var logLevel = parseResult.GetValue(s_logLevelOption); + var logLevel = parseResult.GetValue(s_pipelineLogLevelOption); if (!string.IsNullOrEmpty(logLevel)) { baseArgs.AddRange(["--log-level", logLevel!]); diff --git a/src/Aspire.Cli/Commands/DoCommand.cs b/src/Aspire.Cli/Commands/DoCommand.cs index 6b4bbacfe9f..51922787000 100644 --- a/src/Aspire.Cli/Commands/DoCommand.cs +++ b/src/Aspire.Cli/Commands/DoCommand.cs @@ -76,7 +76,7 @@ protected override async Task GetRunArgumentsAsync(string? fullyQualif } // Add --log-level and --environment flags if specified - var logLevel = parseResult.GetValue(s_logLevelOption); + var logLevel = parseResult.GetValue(s_pipelineLogLevelOption); if (!string.IsNullOrEmpty(logLevel)) { baseArgs.AddRange(["--log-level", logLevel!]); diff --git a/src/Aspire.Cli/Commands/PipelineCommandBase.cs b/src/Aspire.Cli/Commands/PipelineCommandBase.cs index fd9501d6a49..fc1bb7a0ea4 100644 --- a/src/Aspire.Cli/Commands/PipelineCommandBase.cs +++ b/src/Aspire.Cli/Commands/PipelineCommandBase.cs @@ -42,7 +42,7 @@ internal abstract class PipelineCommandBase : BaseCommand private readonly Option _outputPathOption; private readonly Option? _startDebugSessionOption; - protected static readonly Option s_logLevelOption = new("--log-level") + protected static readonly Option s_pipelineLogLevelOption = new("--pipeline-log-level") { Description = SharedCommandStrings.PipelineLogLevelOptionDescription }; @@ -98,7 +98,7 @@ protected PipelineCommandBase(string name, string description, IDotNetCliRunner Options.Add(s_appHostOption); Options.Add(_outputPathOption); - Options.Add(s_logLevelOption); + Options.Add(s_pipelineLogLevelOption); Options.Add(s_environmentOption); Options.Add(s_includeExceptionDetailsOption); Options.Add(s_noBuildOption); @@ -291,7 +291,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken); // Check if debug or trace logging is enabled - var logLevel = parseResult.GetValue(s_logLevelOption); + var logLevel = parseResult.GetValue(s_pipelineLogLevelOption); var isDebugOrTraceLoggingEnabled = logLevel?.Equals("debug", StringComparison.OrdinalIgnoreCase) == true || logLevel?.Equals("trace", StringComparison.OrdinalIgnoreCase) == true; diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index fa49949f61a..c471afb12ea 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -59,7 +59,7 @@ protected override Task GetRunArgumentsAsync(string? fullyQualifiedOut } // Add --log-level and --envionment flags if specified - var logLevel = parseResult.GetValue(s_logLevelOption); + var logLevel = parseResult.GetValue(s_pipelineLogLevelOption); if (!string.IsNullOrEmpty(logLevel)) { diff --git a/src/Aspire.Cli/Resources/ConsoleActivityLoggerStrings.resx b/src/Aspire.Cli/Resources/ConsoleActivityLoggerStrings.resx index bc94d5ec25b..ae77a331101 100644 --- a/src/Aspire.Cli/Resources/ConsoleActivityLoggerStrings.resx +++ b/src/Aspire.Cli/Resources/ConsoleActivityLoggerStrings.resx @@ -142,7 +142,7 @@ Total time: {0} - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. Pipeline succeeded diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.cs.xlf index e3e0fc298d0..33e925f96f4 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.cs.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.de.xlf index 06c34c4d90c..246b75c6444 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.de.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.es.xlf index e79324d9024..e72da85d15c 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.es.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.fr.xlf index 6cd3079bc6c..75d1b73631b 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.fr.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.it.xlf index 7dbff355dac..4c341dc69a9 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.it.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ja.xlf index 4af67e91e1c..dd9fc4c55f1 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ja.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ko.xlf index c3b71aad0a5..8c1742b8795 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ko.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pl.xlf index 5880d393b36..27dd79642cc 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pl.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pt-BR.xlf index 15e925dca42..f713f03452a 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.pt-BR.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ru.xlf index 87d4549ec29..c6a0ec261c8 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.ru.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.tr.xlf index 7ef79f49636..78b980245b0 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.tr.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hans.xlf index 5f2913d3015..880a8c3b544 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hans.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hant.xlf index 546b51d340a..6d87125da9b 100644 --- a/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/ConsoleActivityLoggerStrings.zh-Hant.xlf @@ -28,8 +28,8 @@ - For more details, add --log-level debug/trace to the command. - For more details, add --log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. + For more details, add --pipeline-log-level debug/trace to the command. diff --git a/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs index 2ad0bf5c3bc..e83e015bb43 100644 --- a/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs @@ -509,4 +509,111 @@ public async Task DoCommandWithHelpShowsListStepsOption() var exitCode = await result.InvokeAsync().DefaultTimeout(); Assert.Equal(0, exitCode); } + + [Fact] + public async Task DoCommandForwardsPipelineLogLevelAsLogLevelToAppHost() + { + using var tempRepo = TemporaryWorkspace.Create(outputHelper); + + string[]? capturedArgs = null; + + var services = CliTestHelper.CreateServiceCollection(tempRepo, outputHelper, options => + { + options.ProjectLocatorFactory = (sp) => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner + { + BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0, + + GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) => + { + return (0, true, VersionHelper.GetDefaultTemplateVersion()); + }, + + RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) => + { + capturedArgs = args; + + var completed = new TaskCompletionSource(); + var backchannel = new TestAppHostBackchannel + { + RequestStopAsyncCalled = completed + }; + backchannelCompletionSource?.SetResult(backchannel); + await completed.Task.DefaultTimeout(); + return 0; + } + }; + + return runner; + }; + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + + var result = command.Parse("do my-step --pipeline-log-level debug"); + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(0, exitCode); + Assert.NotNull(capturedArgs); + var logLevelIndex = Array.IndexOf(capturedArgs, "--log-level"); + Assert.True(logLevelIndex >= 0, "Expected --log-level argument to be passed to AppHost"); + Assert.Equal("debug", capturedArgs[logLevelIndex + 1]); + Assert.DoesNotContain("--pipeline-log-level", capturedArgs); + } + + [Fact] + public async Task DoCommandDoesNotForwardCliLogLevelToAppHost() + { + using var tempRepo = TemporaryWorkspace.Create(outputHelper); + + string[]? capturedArgs = null; + + var services = CliTestHelper.CreateServiceCollection(tempRepo, outputHelper, options => + { + options.ProjectLocatorFactory = (sp) => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner + { + BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0, + + GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) => + { + return (0, true, VersionHelper.GetDefaultTemplateVersion()); + }, + + RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) => + { + capturedArgs = args; + + var completed = new TaskCompletionSource(); + var backchannel = new TestAppHostBackchannel + { + RequestStopAsyncCalled = completed + }; + backchannelCompletionSource?.SetResult(backchannel); + await completed.Task.DefaultTimeout(); + return 0; + } + }; + + return runner; + }; + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + + var result = command.Parse("do my-step --log-level Warning"); + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(0, exitCode); + Assert.NotNull(capturedArgs); + Assert.DoesNotContain("--log-level", capturedArgs); + } } From 75c1090552106a463d5f5d570c677fa09f80055f Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 09:17:48 -0700 Subject: [PATCH 11/55] [release/13.3] Fallback to junctions if either creating OR evaluating symlinks fails (#16618) * Fallback to junctions if either creating OR evaluating symlinks fails * Update src/Aspire.Cli/Utils/ReparsePoint.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update outdated test verified results * Regenerate the verified file --------- Co-authored-by: David Negstad Co-authored-by: David Negstad <50252651+danegsta@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Utils/ReparsePoint.cs | 121 +++++++++-- .../Utils/ReparsePointTests.cs | 51 ++++- ...ps_CreatesCorrectDependencies.verified.txt | 199 +++++++++--------- 3 files changed, 262 insertions(+), 109 deletions(-) diff --git a/src/Aspire.Cli/Utils/ReparsePoint.cs b/src/Aspire.Cli/Utils/ReparsePoint.cs index f316dc762fc..b6ad2305b58 100644 --- a/src/Aspire.Cli/Utils/ReparsePoint.cs +++ b/src/Aspire.Cli/Utils/ReparsePoint.cs @@ -15,9 +15,9 @@ namespace Aspire.Cli.Utils; /// Windows strategy: prefer a symbolic link () /// — available to users with Developer Mode or admin — and fall back to a directory /// junction (created via DeviceIoControl + FSCTL_SET_REPARSE_POINT) -/// when symlink creation is denied. Junctions need no elevation, work for local -/// directory targets, and are transparent to -/// and file enumeration. +/// when symlink creation is denied or the created symlink cannot be evaluated. +/// Junctions need no elevation, work for local directory targets, and are +/// transparent to and file enumeration. /// /// Unix strategy: symbolic link via . /// @@ -29,13 +29,13 @@ internal static partial class ReparsePoint /// /// /// The target must be a local directory path. On Windows, if symbolic-link - /// creation is denied (for example, the user does not have Developer Mode - /// enabled and is not running as admin), this method falls back to creating - /// a directory junction. The public behavior is otherwise identical: the - /// resulting path resolves to for I/O purposes. + /// creation is denied or the created symbolic link cannot be evaluated, this + /// method falls back to creating a directory junction. The public behavior is + /// otherwise identical: the resulting path resolves to + /// for I/O purposes. /// /// The path to create the reparse point at. - /// Absolute path to the target directory. + /// Path to the target directory. Relative paths are resolved against the link's parent directory. public static void CreateOrReplace(string linkPath, string target) { if (string.IsNullOrEmpty(linkPath)) @@ -48,7 +48,7 @@ public static void CreateOrReplace(string linkPath, string target) throw new ArgumentException("Target path is required.", nameof(target)); } - var absoluteTarget = Path.GetFullPath(target); + var absoluteTarget = ResolveTargetPath(linkPath, target); // Create the new reparse point under a temporary name adjacent to the // final link, then atomically rename over the existing link. This avoids @@ -113,6 +113,15 @@ public static bool Exists(string path) /// public static bool IsReparsePoint(string path) { + try + { + var attributes = File.GetAttributes(path); + return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + } + try { var info = new FileInfo(path); @@ -142,14 +151,20 @@ public static bool IsReparsePoint(string path) { try { + var attributes = File.GetAttributes(path); + if ((attributes & FileAttributes.ReparsePoint) != FileAttributes.ReparsePoint) + { + return null; + } + var dirInfo = new DirectoryInfo(path); - if (dirInfo.Exists && (dirInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + if (!string.IsNullOrEmpty(dirInfo.LinkTarget)) { return dirInfo.LinkTarget; } var fileInfo = new FileInfo(path); - if (fileInfo.Exists && (fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + if (!string.IsNullOrEmpty(fileInfo.LinkTarget)) { return fileInfo.LinkTarget; } @@ -168,6 +183,38 @@ public static bool IsReparsePoint(string path) /// public static void RemoveIfExists(string path) { + try + { + var attributes = File.GetAttributes(path); + if ((attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + { + if ((attributes & FileAttributes.Directory) == FileAttributes.Directory) + { + Directory.Delete(path); + } + else + { + File.Delete(path); + } + + return; + } + } + catch (DirectoryNotFoundException) + { + return; + } + catch (FileNotFoundException) + { + return; + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + try { var dirInfo = new DirectoryInfo(path); @@ -207,20 +254,43 @@ private static void CreateSymlinkOrJunction(string linkPath, string target) return; } - // Windows: try symbolic link first; fall back to a junction if creation is denied. + // Windows: try symbolic link first; fall back to a junction if creation is denied + // or if Windows policy allows creation but prevents following this link type. try { Directory.CreateSymbolicLink(linkPath, target); - return; + if (CanFollowDirectoryReparsePoint(linkPath)) + { + return; + } + + RemoveIfExists(linkPath); } catch (Exception ex) when (ex is UnauthorizedAccessException or IOException) { + RemoveIfExists(linkPath); // Fall through to junction creation below. } CreateWindowsJunction(linkPath, target); } + internal static bool CanFollowDirectoryReparsePoint(string path) + { + try + { + // Force Windows to evaluate the link immediately. Directory.Exists can + // report true for a symlink whose evaluation class is disabled. + using var enumerator = Directory.EnumerateFileSystemEntries(path).GetEnumerator(); + _ = enumerator.MoveNext(); + return true; + } + catch + { + return false; + } + } + private static string GetTempLinkPath(string linkPath) { // Use an adjacent path under the same parent so the rename stays on-volume @@ -231,6 +301,31 @@ private static string GetTempLinkPath(string linkPath) return Path.Combine(parent, $"{name}.new.{suffix}"); } + internal static string ResolveTargetPath(string linkPath, string target) + { + var normalizedTarget = NormalizeWindowsTargetPath(target); + if (Path.IsPathFullyQualified(normalizedTarget)) + { + return Path.GetFullPath(normalizedTarget); + } + + var linkParent = Path.GetDirectoryName(Path.GetFullPath(linkPath)) ?? "."; + return Path.GetFullPath(Path.Combine(linkParent, normalizedTarget)); + } + + private static string NormalizeWindowsTargetPath(string target) + { + const string ntLocalPathPrefix = @"\??\"; + if (OperatingSystem.IsWindows() && + target.StartsWith(ntLocalPathPrefix, StringComparison.Ordinal) && + target.Length > ntLocalPathPrefix.Length) + { + return target[ntLocalPathPrefix.Length..]; + } + + return target; + } + // ═══════════════════════════════════════════════════════════════════════ // Windows junction fallback (no admin / dev-mode required) // ═══════════════════════════════════════════════════════════════════════ diff --git a/tests/Aspire.Cli.Tests/Utils/ReparsePointTests.cs b/tests/Aspire.Cli.Tests/Utils/ReparsePointTests.cs index d41eace944b..880c21ed878 100644 --- a/tests/Aspire.Cli.Tests/Utils/ReparsePointTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/ReparsePointTests.cs @@ -128,6 +128,47 @@ public void RemoveIfExists_DoesNothingForMissingPath() ReparsePoint.RemoveIfExists(Path.Combine(workspace.WorkspaceRoot.FullName, "missing")); } + [Fact] + public void ResolveTargetPath_ResolvesRelativeTargetAgainstLinkDirectory() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var link = Path.Combine(root, "bundle"); + var target = Path.Combine(root, "versions", "v1"); + + var resolvedTarget = ReparsePoint.ResolveTargetPath(link, Path.Combine("versions", "v1")); + + Assert.Equal(Path.GetFullPath(target), resolvedTarget); + } + + [Fact] + public void CanFollowDirectoryReparsePoint_ReturnsFalseWhenSymlinkTargetCannotBeOpened() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var link = Path.Combine(root, "bundle"); + try + { + Directory.CreateSymbolicLink(link, Path.Combine("versions", "missing")); + } + catch (Exception ex) when (ex is UnauthorizedAccessException or IOException) + { + Assert.Skip("Symlink creation is not available (Developer Mode not enabled or not running as admin)."); + return; + } + + try + { + Assert.False(ReparsePoint.CanFollowDirectoryReparsePoint(link)); + } + finally + { + ReparsePoint.RemoveIfExists(link); + } + } + // ───────────────────────────────────────────────────────────────────── // Windows-specific: explicitly exercise the junction code path. // @@ -338,14 +379,20 @@ public void CreateOrReplace_MigratesJunctionToSymlink_WhenSymlinksAreAvailable() using var workspace = TemporaryWorkspace.Create(outputHelper); var root = workspace.WorkspaceRoot.FullName; - // Probe: can we create symlinks on this machine? If not, skip — - // we cannot assert a symlink was created. + // Probe: can we create and evaluate symlinks on this machine? If not, skip — + // CreateOrReplace should fall back to a junction and this test cannot assert + // that a symlink was created. var probe = Path.Combine(root, "symlink-probe"); var probeTarget = Path.Combine(root, "probe-target"); Directory.CreateDirectory(probeTarget); try { Directory.CreateSymbolicLink(probe, probeTarget); + if (!ReparsePoint.CanFollowDirectoryReparsePoint(probe)) + { + Assert.Skip("Symlink evaluation is not available on this machine."); + return; + } } catch (Exception ex) when (ex is UnauthorizedAccessException or IOException) { diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt index 9964a2d210c..99c37dc1b39 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithFoundryAndAzureContainerApps_CreatesCorrectDependencies.verified.txt @@ -5,7 +5,7 @@ PIPELINE DEPENDENCY GRAPH DIAGNOSTICS This diagnostic output shows the complete pipeline dependency graph structure. Use this to understand step relationships and troubleshoot execution issues. -Total steps defined: 42 +Total steps defined: 43 Analysis for full pipeline execution (showing all steps and their relationships) @@ -23,39 +23,40 @@ Steps with no dependencies run first, followed by steps that depend on them. 7. process-parameters 8. build-prereq 9. check-container-runtime - 10. deploy-prereq - 11. build-agent - 12. build-api - 13. build - 14. validate-azure-login - 15. create-provisioning-context - 16. provision-aca-env-acr - 17. provision-aca-env - 18. login-to-acr-aca-env-acr - 19. provision-foundry-project-acr - 20. login-to-acr-foundry-project-acr - 21. push-prereq - 22. push-api - 23. provision-api-containerapp - 24. provision-foundry - 25. provision-foundry-project - 26. provision-azure-bicep-resources - 27. compute-endpoints-foundry-project - 28. push-agent - 29. deploy-agent-ha - 30. print-api-summary - 31. print-dashboard-url-aca-env - 32. deploy - 33. deploy-api - 34. destroy-prereq - 35. destroy-azure-azure634f9 - 36. destroy - 37. diagnostics - 38. publish-prereq - 39. publish-azure634f9 - 40. publish - 41. publish-manifest - 42. push + 10. validate-build-only-container-references + 11. deploy-prereq + 12. build-agent + 13. build-api + 14. build + 15. validate-azure-login + 16. create-provisioning-context + 17. provision-aca-env-acr + 18. provision-aca-env + 19. login-to-acr-aca-env-acr + 20. provision-foundry-project-acr + 21. login-to-acr-foundry-project-acr + 22. push-prereq + 23. push-api + 24. provision-api-containerapp + 25. provision-foundry + 26. provision-foundry-project + 27. provision-azure-bicep-resources + 28. compute-endpoints-foundry-project + 29. push-agent + 30. deploy-agent-ha + 31. print-api-summary + 32. print-dashboard-url-aca-env + 33. deploy + 34. deploy-api + 35. destroy-prereq + 36. destroy-azure-azure634f9 + 37. destroy + 38. diagnostics + 39. publish-prereq + 40. publish-azure634f9 + 41. publish + 42. publish-manifest + 43. push DETAILED STEP ANALYSIS ====================== @@ -121,7 +122,7 @@ Step: deploy-api Step: deploy-prereq Description: Prerequisite step that runs before any deploy operations. Initializes deployment environment and manages deployment state. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: destroy Description: Aggregation step for all destroy operations. All destroy steps should be required by this step. @@ -233,7 +234,7 @@ Step: publish-manifest Step: publish-prereq Description: Prerequisite step that runs before any publish operations. - Dependencies: ✓ process-parameters + Dependencies: ✓ process-parameters, ✓ validate-build-only-container-references Step: push Description: Aggregation step for all push operations. All push steps should be required by this step. @@ -261,6 +262,10 @@ Step: validate-azure-login Dependencies: ✓ deploy-prereq Resource: azure634f9 (AzureEnvironmentResource) +Step: validate-build-only-container-references + Description: Validates that build-only containers are consumed by another resource before publish or deploy. + Dependencies: none + Step: validate-compute-environments Description: Validates compute resource bindings before startup. Dependencies: none @@ -292,26 +297,26 @@ If targeting 'before-start': If targeting 'build': Direct dependencies: build-agent, build-api - Total steps: 7 + Total steps: 8 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-agent | build-api (parallel) [3] build If targeting 'build-agent': Direct dependencies: build-prereq, check-container-runtime, deploy-prereq - Total steps: 5 + Total steps: 6 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-agent If targeting 'build-api': Direct dependencies: build-prereq, check-container-runtime, deploy-prereq - Total steps: 5 + Total steps: 6 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api @@ -330,9 +335,9 @@ If targeting 'check-container-runtime': If targeting 'compute-endpoints-foundry-project': Direct dependencies: provision-azure-bicep-resources - Total steps: 19 + Total steps: 20 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -346,18 +351,18 @@ If targeting 'compute-endpoints-foundry-project': If targeting 'create-provisioning-context': Direct dependencies: deploy-prereq, validate-azure-login - Total steps: 4 + Total steps: 5 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context If targeting 'deploy': Direct dependencies: build-agent, build-api, compute-endpoints-foundry-project, create-provisioning-context, deploy-agent-ha, print-api-summary, print-dashboard-url-aca-env, provision-azure-bicep-resources, validate-azure-login - Total steps: 25 + Total steps: 26 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-agent | build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -372,9 +377,9 @@ If targeting 'deploy': If targeting 'deploy-agent-ha': Direct dependencies: deploy-prereq, provision-azure-bicep-resources, push-agent - Total steps: 21 + Total steps: 22 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-agent | build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -388,9 +393,9 @@ If targeting 'deploy-agent-ha': If targeting 'deploy-api': Direct dependencies: print-api-summary - Total steps: 17 + Total steps: 18 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -403,10 +408,10 @@ If targeting 'deploy-api': [10] deploy-api If targeting 'deploy-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq If targeting 'destroy': @@ -438,9 +443,9 @@ If targeting 'diagnostics': If targeting 'login-to-acr-aca-env-acr': Direct dependencies: provision-aca-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -449,9 +454,9 @@ If targeting 'login-to-acr-aca-env-acr': If targeting 'login-to-acr-foundry-project-acr': Direct dependencies: provision-foundry-project-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -474,9 +479,9 @@ If targeting 'prepare-foundry-project-foundry-project': If targeting 'print-api-summary': Direct dependencies: provision-api-containerapp - Total steps: 16 + Total steps: 17 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -489,9 +494,9 @@ If targeting 'print-api-summary': If targeting 'print-dashboard-url-aca-env': Direct dependencies: provision-aca-env, provision-azure-bicep-resources - Total steps: 19 + Total steps: 20 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -511,9 +516,9 @@ If targeting 'process-parameters': If targeting 'provision-aca-env': Direct dependencies: create-provisioning-context, provision-aca-env-acr - Total steps: 6 + Total steps: 7 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -522,9 +527,9 @@ If targeting 'provision-aca-env': If targeting 'provision-aca-env-acr': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -532,9 +537,9 @@ If targeting 'provision-aca-env-acr': If targeting 'provision-api-containerapp': Direct dependencies: create-provisioning-context, provision-aca-env, push-api - Total steps: 15 + Total steps: 16 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -546,9 +551,9 @@ If targeting 'provision-api-containerapp': If targeting 'provision-azure-bicep-resources': Direct dependencies: create-provisioning-context, deploy-prereq, provision-aca-env, provision-aca-env-acr, provision-api-containerapp, provision-foundry, provision-foundry-project, provision-foundry-project-acr - Total steps: 18 + Total steps: 19 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -561,9 +566,9 @@ If targeting 'provision-azure-bicep-resources': If targeting 'provision-foundry': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -571,9 +576,9 @@ If targeting 'provision-foundry': If targeting 'provision-foundry-project': Direct dependencies: create-provisioning-context, provision-foundry, provision-foundry-project-acr - Total steps: 7 + Total steps: 8 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -582,9 +587,9 @@ If targeting 'provision-foundry-project': If targeting 'provision-foundry-project-acr': Direct dependencies: create-provisioning-context - Total steps: 5 + Total steps: 6 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -592,18 +597,18 @@ If targeting 'provision-foundry-project-acr': If targeting 'publish': Direct dependencies: publish-azure634f9 - Total steps: 4 + Total steps: 5 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 [3] publish If targeting 'publish-azure634f9': Direct dependencies: publish-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq [2] publish-azure634f9 @@ -614,17 +619,17 @@ If targeting 'publish-manifest': [0] publish-manifest If targeting 'publish-prereq': - Direct dependencies: process-parameters - Total steps: 2 + Direct dependencies: process-parameters, validate-build-only-container-references + Total steps: 3 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] publish-prereq If targeting 'push': Direct dependencies: push-agent, push-api, push-prereq - Total steps: 16 + Total steps: 17 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-agent | build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -636,9 +641,9 @@ If targeting 'push': If targeting 'push-agent': Direct dependencies: build-agent, push-prereq - Total steps: 13 + Total steps: 14 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-agent | validate-azure-login (parallel) [3] create-provisioning-context @@ -649,9 +654,9 @@ If targeting 'push-agent': If targeting 'push-api': Direct dependencies: build-api, push-prereq - Total steps: 13 + Total steps: 14 Execution order: - [0] check-container-runtime | process-parameters (parallel) + [0] check-container-runtime | process-parameters | validate-build-only-container-references (parallel) [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context @@ -662,9 +667,9 @@ If targeting 'push-api': If targeting 'push-prereq': Direct dependencies: login-to-acr-aca-env-acr, login-to-acr-foundry-project-acr - Total steps: 9 + Total steps: 10 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context @@ -680,12 +685,18 @@ If targeting 'validate-azure-container-apps': If targeting 'validate-azure-login': Direct dependencies: deploy-prereq - Total steps: 3 + Total steps: 4 Execution order: - [0] process-parameters + [0] process-parameters | validate-build-only-container-references (parallel) [1] deploy-prereq [2] validate-azure-login +If targeting 'validate-build-only-container-references': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] validate-build-only-container-references + If targeting 'validate-compute-environments': Direct dependencies: none Total steps: 1 From 37f41c00d5267bb1a4238187b15e112a14ee6372 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 11:30:30 -0700 Subject: [PATCH 12/55] [release/13.3] Fix TypeScript AppHost generated port ranges (#16649) * Fix TypeScript AppHost generated port ranges Use a shared AppHost profile port generator for CLI templates, init, and TypeScript AppHost scaffolding so generated dashboard and service profile ports avoid the Windows ephemeral range. Add regression coverage for the generated TypeScript apphost.run.json ports. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * PR feedback --------- Co-authored-by: Eric Erhardt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Aspire.Cli.csproj | 1 + src/Aspire.Cli/Commands/InitCommand.cs | 21 ++++----- .../Templating/CliTemplateFactory.cs | 26 ++++------- ...e.Hosting.CodeGeneration.TypeScript.csproj | 1 + .../TypeScriptLanguageSupport.cs | 11 ++--- src/Shared/AppHostProfilePortGenerator.cs | 41 +++++++++++++++++ .../TypeScriptLanguageSupportTests.cs | 46 +++++++++++++++++++ 7 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 src/Shared/AppHostProfilePortGenerator.cs diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj index aec002100aa..b29804475cb 100644 --- a/src/Aspire.Cli/Aspire.Cli.csproj +++ b/src/Aspire.Cli/Aspire.Cli.csproj @@ -93,6 +93,7 @@ + diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index e643237737f..e816f491db4 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -15,6 +15,7 @@ using Aspire.Cli.Scaffolding; using Aspire.Cli.Telemetry; using Aspire.Cli.Utils; +using Aspire.Shared; namespace Aspire.Cli.Commands; @@ -398,32 +399,26 @@ private int DropAspireConfig(DirectoryInfo directory, string appHostPath, string // Normally scaffolding + codegen creates these, but our thin init skips scaffolding. if (settings["profiles"] is null) { - // Port ranges match CliTemplateFactory.GenerateRandomPorts() - var httpPort = Random.Shared.Next(15000, 15300); - var httpsPort = Random.Shared.Next(17000, 17300); - var otlpHttpPort = Random.Shared.Next(19000, 19300); - var otlpHttpsPort = Random.Shared.Next(21000, 21300); - var resourceHttpPort = Random.Shared.Next(20000, 20300); - var resourceHttpsPort = Random.Shared.Next(22000, 22300); + var ports = AppHostProfilePortGenerator.Generate(Random.Shared); settings["profiles"] = new JsonObject { ["https"] = new JsonObject { - ["applicationUrl"] = $"https://localhost:{httpsPort};http://localhost:{httpPort}", + ["applicationUrl"] = $"https://localhost:{ports.DashboardHttpsPort};http://localhost:{ports.DashboardHttpPort}", ["environmentVariables"] = new JsonObject { - ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"https://localhost:{otlpHttpsPort}", - ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"https://localhost:{resourceHttpsPort}" + ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"https://localhost:{ports.OtlpHttpsPort}", + ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"https://localhost:{ports.ResourceServiceHttpsPort}" } }, ["http"] = new JsonObject { - ["applicationUrl"] = $"http://localhost:{httpPort}", + ["applicationUrl"] = $"http://localhost:{ports.DashboardHttpPort}", ["environmentVariables"] = new JsonObject { - ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"http://localhost:{otlpHttpPort}", - ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"http://localhost:{resourceHttpPort}", + ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"http://localhost:{ports.OtlpHttpPort}", + ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"http://localhost:{ports.ResourceServiceHttpPort}", ["ASPIRE_ALLOW_UNSECURED_TRANSPORT"] = "true" } } diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.cs index 82f84dfed17..098592a8848 100644 --- a/src/Aspire.Cli/Templating/CliTemplateFactory.cs +++ b/src/Aspire.Cli/Templating/CliTemplateFactory.cs @@ -10,6 +10,7 @@ using Aspire.Cli.Resources; using Aspire.Cli.Scaffolding; using Aspire.Cli.Utils; +using Aspire.Shared; using Microsoft.Extensions.Logging; namespace Aspire.Cli.Templating; @@ -164,37 +165,26 @@ private bool IsTemplateAvailable(ITemplate template) return _languageDiscovery.GetLanguageById(new LanguageId(template.LanguageId)) is not null; } - private static string ApplyTokens(string content, string projectName, string projectNameLower, string aspireVersion, TemplatePorts ports, string hostName = "localhost") + private static string ApplyTokens(string content, string projectName, string projectNameLower, string aspireVersion, AppHostProfilePorts ports, string hostName = "localhost") { return content .Replace("{{projectName}}", projectName) .Replace("{{projectNameLower}}", projectNameLower) .Replace("{{aspireVersion}}", aspireVersion) .Replace("{{hostName}}", hostName) - .Replace("{{httpPort}}", ports.HttpPort.ToString(CultureInfo.InvariantCulture)) - .Replace("{{httpsPort}}", ports.HttpsPort.ToString(CultureInfo.InvariantCulture)) + .Replace("{{httpPort}}", ports.DashboardHttpPort.ToString(CultureInfo.InvariantCulture)) + .Replace("{{httpsPort}}", ports.DashboardHttpsPort.ToString(CultureInfo.InvariantCulture)) .Replace("{{otlpHttpPort}}", ports.OtlpHttpPort.ToString(CultureInfo.InvariantCulture)) .Replace("{{otlpHttpsPort}}", ports.OtlpHttpsPort.ToString(CultureInfo.InvariantCulture)) - .Replace("{{resourceHttpPort}}", ports.ResourceHttpPort.ToString(CultureInfo.InvariantCulture)) - .Replace("{{resourceHttpsPort}}", ports.ResourceHttpsPort.ToString(CultureInfo.InvariantCulture)); + .Replace("{{resourceHttpPort}}", ports.ResourceServiceHttpPort.ToString(CultureInfo.InvariantCulture)) + .Replace("{{resourceHttpsPort}}", ports.ResourceServiceHttpsPort.ToString(CultureInfo.InvariantCulture)); } - private static TemplatePorts GenerateRandomPorts() + private static AppHostProfilePorts GenerateRandomPorts() { - return new TemplatePorts( - HttpPort: Random.Shared.Next(15000, 15300), - HttpsPort: Random.Shared.Next(17000, 17300), - OtlpHttpPort: Random.Shared.Next(19000, 19300), - OtlpHttpsPort: Random.Shared.Next(21000, 21300), - ResourceHttpPort: Random.Shared.Next(20000, 20300), - ResourceHttpsPort: Random.Shared.Next(22000, 22300)); + return AppHostProfilePortGenerator.Generate(Random.Shared); } - private sealed record TemplatePorts( - int HttpPort, int HttpsPort, - int OtlpHttpPort, int OtlpHttpsPort, - int ResourceHttpPort, int ResourceHttpsPort); - private static void AddOptionIfMissing(System.CommandLine.Command command, System.CommandLine.Option option) { if (!command.Options.Contains(option)) diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj b/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj index 44bd6c969fb..68bd97d52c9 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs index 06f5df0cd12..466b9aa1738 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs @@ -114,19 +114,16 @@ public Dictionary Scaffold(ScaffoldRequest request) ? new Random(request.PortSeed.Value) : Random.Shared; - var httpsPort = random.Next(10000, 65000); - var httpPort = random.Next(10000, 65000); - var otlpPort = random.Next(10000, 65000); - var resourceServicePort = random.Next(10000, 65000); + var ports = AppHostProfilePortGenerator.Generate(random); files["apphost.run.json"] = $$""" { "profiles": { "https": { - "applicationUrl": "https://localhost:{{httpsPort}};http://localhost:{{httpPort}}", + "applicationUrl": "https://localhost:{{ports.DashboardHttpsPort}};http://localhost:{{ports.DashboardHttpPort}}", "environmentVariables": { - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{otlpPort}}", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{resourceServicePort}}" + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{ports.OtlpHttpsPort}}", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{ports.ResourceServiceHttpsPort}}" } } } diff --git a/src/Shared/AppHostProfilePortGenerator.cs b/src/Shared/AppHostProfilePortGenerator.cs new file mode 100644 index 00000000000..f9516b5d734 --- /dev/null +++ b/src/Shared/AppHostProfilePortGenerator.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Shared; + +internal static class AppHostProfilePortGenerator +{ + internal const int DashboardHttpPortMin = 15000; + internal const int DashboardHttpPortMaxExclusive = 15300; + internal const int DashboardHttpsPortMin = 17000; + internal const int DashboardHttpsPortMaxExclusive = 17300; + internal const int OtlpHttpPortMin = 19000; + internal const int OtlpHttpPortMaxExclusive = 19300; + internal const int OtlpHttpsPortMin = 21000; + internal const int OtlpHttpsPortMaxExclusive = 21300; + internal const int ResourceServiceHttpPortMin = 20000; + internal const int ResourceServiceHttpPortMaxExclusive = 20300; + internal const int ResourceServiceHttpsPortMin = 22000; + internal const int ResourceServiceHttpsPortMaxExclusive = 22300; + + internal static AppHostProfilePorts Generate(Random random) + { + ArgumentNullException.ThrowIfNull(random); + + return new AppHostProfilePorts( + DashboardHttpPort: random.Next(DashboardHttpPortMin, DashboardHttpPortMaxExclusive), + DashboardHttpsPort: random.Next(DashboardHttpsPortMin, DashboardHttpsPortMaxExclusive), + OtlpHttpPort: random.Next(OtlpHttpPortMin, OtlpHttpPortMaxExclusive), + OtlpHttpsPort: random.Next(OtlpHttpsPortMin, OtlpHttpsPortMaxExclusive), + ResourceServiceHttpPort: random.Next(ResourceServiceHttpPortMin, ResourceServiceHttpPortMaxExclusive), + ResourceServiceHttpsPort: random.Next(ResourceServiceHttpsPortMin, ResourceServiceHttpsPortMaxExclusive)); + } +} + +internal readonly record struct AppHostProfilePorts( + int DashboardHttpPort, + int DashboardHttpsPort, + int OtlpHttpPort, + int OtlpHttpsPort, + int ResourceServiceHttpPort, + int ResourceServiceHttpsPort); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs index adaab97bdf7..1b016e144bc 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json.Nodes; +using Aspire.Shared; using Aspire.TypeSystem; namespace Aspire.Hosting.CodeGeneration.TypeScript.Tests; @@ -187,6 +188,41 @@ public void Scaffold_DoesNotEmitRootTsConfig_WhenOneAlreadyExists() Assert.Equal(existingTsConfig, File.ReadAllText(existingTsConfigPath)); } + [Theory] + [InlineData(null)] + [InlineData(0)] + [InlineData(1)] + [InlineData(16626)] + [InlineData(55571)] + public void Scaffold_GeneratesProfilePortsOutsideWindowsEphemeralRange(int? portSeed) + { + using var testDir = new TestTempDirectory(); + + var files = _languageSupport.Scaffold(new ScaffoldRequest + { + TargetPath = testDir.Path, + ProjectName = "PortsApp", + PortSeed = portSeed + }); + + var appHostRunJson = ParseJson(files["apphost.run.json"]); + var httpsProfile = appHostRunJson["profiles"]!["https"]!.AsObject(); + var applicationUrls = httpsProfile["applicationUrl"]!.GetValue().Split(';', StringSplitOptions.RemoveEmptyEntries); + var environmentVariables = httpsProfile["environmentVariables"]!.AsObject(); + + Assert.Equal(2, applicationUrls.Length); + + var httpsPort = GetPort(applicationUrls.Single(url => url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))); + var httpPort = GetPort(applicationUrls.Single(url => url.StartsWith("http://", StringComparison.OrdinalIgnoreCase))); + var otlpHttpsPort = GetPort(environmentVariables["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]!.GetValue()); + var resourceServiceHttpsPort = GetPort(environmentVariables["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]!.GetValue()); + + AssertPortInRange(httpPort, AppHostProfilePortGenerator.DashboardHttpPortMin, AppHostProfilePortGenerator.DashboardHttpPortMaxExclusive); + AssertPortInRange(httpsPort, AppHostProfilePortGenerator.DashboardHttpsPortMin, AppHostProfilePortGenerator.DashboardHttpsPortMaxExclusive); + AssertPortInRange(otlpHttpsPort, AppHostProfilePortGenerator.OtlpHttpsPortMin, AppHostProfilePortGenerator.OtlpHttpsPortMaxExclusive); + AssertPortInRange(resourceServiceHttpsPort, AppHostProfilePortGenerator.ResourceServiceHttpsPortMin, AppHostProfilePortGenerator.ResourceServiceHttpsPortMaxExclusive); + } + [Fact] public void GetRuntimeSpec_UsesAppHostSpecificTsConfig() { @@ -198,4 +234,14 @@ public void GetRuntimeSpec_UsesAppHostSpecificTsConfig() } private static JsonObject ParseJson(string content) => JsonNode.Parse(content)!.AsObject(); + + private static int GetPort(string url) => new Uri(url).Port; + + private const int WindowsEphemeralPortMin = 49152; + + private static void AssertPortInRange(int port, int minInclusive, int maxExclusive) + { + Assert.InRange(port, minInclusive, maxExclusive - 1); + Assert.True(port < WindowsEphemeralPortMin, $"Expected port {port} to be below the Windows ephemeral range."); + } } From 529f2ca3284780a765a51de63d08d807a766708d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 2 May 2026 02:39:23 +0800 Subject: [PATCH 13/55] [release/13.3] Fix unbounded collection growth in TelemetryRepository (#16590) * Fix unbounded collection growth in TelemetryRepository and related types - Add MaxResourceCount option to TelemetryLimitOptions (default 10,000) to cap _resources growth. Throws on limit exceeded. - Clear _logScopes, _logPropertyKeys on ClearStructuredLogs (full clear) and remove per-resource property keys on per-resource clear. - Clear _traceScopes, _tracePropertyKeys, _spanLinks on ClearTraces (full clear) and clean up span links and property keys per-resource. - Clear _meters alongside _instruments in OtlpResource.ClearMetrics. - Add internal const limits on TelemetryRepository for resource views (10,000), instruments (10,000), dimensions (10,000), known attribute value keys (10,000), and values per key (10,000). - Enforce instrument limit in OtlpResource.AddMetrics. - Enforce resource view limit in OtlpResource.GetView. - Enforce dimension limit in OtlpInstrument.FindScope. - Cap KnownAttributeValues keys and per-key value lists in OtlpInstrument.CreateDimensionScope. - Add clarifying comments to fields describing their bounds. * Add limit enforcement tests and fix uncaught exceptions from resource limit - Add TelemetryLimitTests with 5 tests for resource and instrument limits - Add maxResourceCount parameter to CreateRepository test helper - Wrap GetOrAddResource calls in GetPeerResource (return null), CalculateTraceUninstrumentedPeers, and OnPeerChanged with try/catch - Fix _meters comment to not claim an unenforced bound - Document TOCTOU soft-cap behavior on resource limit check * Reset HasTraces/HasLogs and clear unviewed error logs on full clear - ClearTraces full-clear now resets HasTraces on all resources - ClearStructuredLogs full-clear now resets HasLogs on all resources and clears _resourceUnviewedErrorLogs * Remove redundant per-telemetry-type resource limit tests * Add scope and instrument limits with TryGetValue pattern - Add MaxScopeCount limit to TryGetOrAddScope, using TryGetValue instead of GetValueRefOrAddDefault to avoid add-then-remove on overflow - Refactor instrument add in OtlpResource to use TryGetValue + count check before inserting, removing the add-then-remove pattern - Fix AddLogs failure count: count log records, not scopes - Fix AddMetrics failure count: count data points, not metrics - Add tests for resource limit, scope limit, and correct failure counting --- .../Configuration/DashboardOptions.cs | 1 + .../Otlp/Model/OtlpHelpers.cs | 22 +- .../Otlp/Model/OtlpInstrument.cs | 23 +- .../Otlp/Model/OtlpResource.cs | 33 +- .../Otlp/Storage/TelemetryRepository.cs | 96 +++- .../TelemetryLimitTests.cs | 502 ++++++++++++++++++ .../Shared/Telemetry/TelemetryTestHelpers.cs | 5 + 7 files changed, 646 insertions(+), 36 deletions(-) create mode 100644 tests/Aspire.Dashboard.Tests/TelemetryRepositoryTests/TelemetryLimitTests.cs diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 8c5e8bc55c0..d890a3dcb2a 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -303,6 +303,7 @@ public sealed class TelemetryLimitOptions public int MaxAttributeCount { get; set; } = 128; public int MaxAttributeLength { get; set; } = int.MaxValue; public int MaxSpanEventCount { get; set; } = int.MaxValue; + public int MaxResourceCount { get; set; } = 10_000; } public sealed class UIOptions diff --git a/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs b/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs index 2e060587d66..07da1018db5 100644 --- a/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs +++ b/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; @@ -467,18 +466,23 @@ public static bool TryGetOrAddScope(Dictionary scopes, Instru // Semantically when InstrumentationScope isn't set, it is equivalent with // an empty instrumentation scope name (unknown). var name = scope?.Name ?? string.Empty; - ref var scopeRef = ref CollectionsMarshal.GetValueRefOrAddDefault(scopes, name, out _); - // Adds to dictionary if not present. - if (scopeRef == null) + if (scopes.TryGetValue(name, out s)) { - scopeRef = (scope != null) - ? new OtlpScope(scope.Name, scope.Version, scope.Attributes.ToKeyValuePairs(context)) - : OtlpScope.Empty; + return true; + } - context.Logger.LogTrace("Added scope '{ScopeName}' to {TelemetryType}.", scopeRef.Name, telemetryType); + if (scopes.Count >= TelemetryRepository.MaxScopeCount) + { + throw new InvalidOperationException($"Scope limit of {TelemetryRepository.MaxScopeCount} reached for {telemetryType}. Scope '{name}' will not be added."); } - s = scopeRef; + s = (scope != null) + ? new OtlpScope(scope.Name, scope.Version, scope.Attributes.ToKeyValuePairs(context)) + : OtlpScope.Empty; + + scopes.Add(name, s); + + context.Logger.LogTrace("Added scope '{ScopeName}' to {TelemetryType}.", s.Name, telemetryType); return true; } catch (Exception ex) diff --git a/src/Aspire.Dashboard/Otlp/Model/OtlpInstrument.cs b/src/Aspire.Dashboard/Otlp/Model/OtlpInstrument.cs index 963e1223a91..5a97d1dce0f 100644 --- a/src/Aspire.Dashboard/Otlp/Model/OtlpInstrument.cs +++ b/src/Aspire.Dashboard/Otlp/Model/OtlpInstrument.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Aspire.Dashboard.Otlp.Model.MetricValues; +using Aspire.Dashboard.Otlp.Storage; using Google.Protobuf.Collections; using OpenTelemetry.Proto.Common.V1; @@ -63,6 +64,11 @@ public DimensionScope FindScope(RepeatedField attributes, ref KeyValue // Need to add dimensions using durable attributes instance after scope is created. if (!Dimensions.TryGetValue(comparableAttributes, out var dimension)) { + if (Dimensions.Count >= TelemetryRepository.MaxDimensionCount) + { + throw new InvalidOperationException($"Dimension limit of {TelemetryRepository.MaxDimensionCount} reached for instrument '{Summary.Name}'."); + } + dimension = CreateDimensionScope(comparableAttributes); Dimensions.Add(dimension.Attributes, dimension); } @@ -78,28 +84,35 @@ private DimensionScope CreateDimensionScope(Memory> var keys = KnownAttributeValues.Keys.Union(durableAttributes.Select(a => a.Key)).Distinct(); foreach (var key in keys) { - ref var values = ref CollectionsMarshal.GetValueRefOrAddDefault(KnownAttributeValues, key, out _); + ref var values = ref CollectionsMarshal.GetValueRefOrAddDefault(KnownAttributeValues, key, out var existed); // Adds to dictionary if not present. if (values == null) { + if (!existed && KnownAttributeValues.Count > TelemetryRepository.MaxKnownAttributeValueCount) + { + // Over limit. Remove the default entry that GetValueRefOrAddDefault added. + KnownAttributeValues.Remove(key); + continue; + } + values = new List(); // If the key is new and there are already dimensions, add an empty value because there are dimensions without this key. if (!isFirst) { - TryAddValue(values, null); + TryAddValue(values, null, TelemetryRepository.MaxKnownAttributeValuesPerKey); } } var currentDimensionValue = OtlpHelpers.GetValue(durableAttributes, key); - TryAddValue(values, currentDimensionValue); + TryAddValue(values, currentDimensionValue, TelemetryRepository.MaxKnownAttributeValuesPerKey); } return dimension; - static void TryAddValue(List values, string? value) + static void TryAddValue(List values, string? value, int maxValues) { - if (!values.Contains(value)) + if (values.Count < maxValues && !values.Contains(value)) { values.Add(value); } diff --git a/src/Aspire.Dashboard/Otlp/Model/OtlpResource.cs b/src/Aspire.Dashboard/Otlp/Model/OtlpResource.cs index c2c058901e4..9a430663769 100644 --- a/src/Aspire.Dashboard/Otlp/Model/OtlpResource.cs +++ b/src/Aspire.Dashboard/Otlp/Model/OtlpResource.cs @@ -4,7 +4,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; using Aspire.Dashboard.Otlp.Storage; using Google.Protobuf.Collections; using OpenTelemetry.Proto.Common.V1; @@ -45,6 +44,7 @@ public class OtlpResource : IOtlpResource public ResourceKey ResourceKey => new ResourceKey(ResourceName, InstanceId); private readonly ReaderWriterLockSlim _metricsLock = new(); + // Bounded by TelemetryRepository.MaxScopeCount. Cleared when metrics are cleared. private readonly Dictionary _meters = new(); private readonly Dictionary _instruments = new(); private readonly ConcurrentDictionary[], OtlpResourceView> _resourceViews = new(ResourceViewKeyComparer.Instance); @@ -70,7 +70,7 @@ public void AddMetrics(AddContext context, RepeatedField scopeMetr { if (!OtlpHelpers.TryGetOrAddScope(_meters, sm.Scope, Context, TelemetryType.Metrics, out var scope)) { - context.FailureCount += sm.Metrics.Count; + context.FailureCount += sm.Metrics.Sum(m => GetMetricDataPointCount(m)); continue; } @@ -86,11 +86,13 @@ public void AddMetrics(AddContext context, RepeatedField scopeMetr } var instrumentKey = new OtlpInstrumentKey(scope.Name, metric.Name); - ref var instrumentRef = ref CollectionsMarshal.GetValueRefOrAddDefault(_instruments, instrumentKey, out _); - if (instrumentRef == null) + if (_instruments.TryGetValue(instrumentKey, out var existingInstrument)) { - // Adds to dictionary if not present. - instrumentRef = new OtlpInstrument + instrument = existingInstrument; + } + else if (_instruments.Count < TelemetryRepository.MaxInstrumentCount) + { + var newInstrument = new OtlpInstrument { Summary = new OtlpInstrumentSummary { @@ -104,10 +106,15 @@ public void AddMetrics(AddContext context, RepeatedField scopeMetr Context = Context }; - Context.Logger.LogTrace("Added metric instrument '{InstrumentName}' for scope '{ScopeName}'.", instrumentRef.Summary.Name, scope.Name); - } + _instruments.Add(instrumentKey, newInstrument); + instrument = newInstrument; - instrument = instrumentRef; + Context.Logger.LogTrace("Added metric instrument '{InstrumentName}' for scope '{ScopeName}'.", instrument.Summary.Name, scope.Name); + } + else + { + throw new InvalidOperationException($"Instrument limit of {TelemetryRepository.MaxInstrumentCount} reached. Instrument '{metric.Name}' will not be added."); + } } catch (Exception ex) { @@ -127,7 +134,7 @@ public void AddMetrics(AddContext context, RepeatedField scopeMetr } } - private static int GetMetricDataPointCount(Metric metric) + internal static int GetMetricDataPointCount(Metric metric) { return metric.DataCase switch { @@ -207,6 +214,7 @@ public void ClearMetrics() try { _instruments.Clear(); + _meters.Clear(); } finally { @@ -296,6 +304,11 @@ internal OtlpResourceView GetView(RepeatedField attributes) return resourceView; } + if (_resourceViews.Count >= TelemetryRepository.MaxResourceViewCount) + { + throw new InvalidOperationException($"Resource view limit of {TelemetryRepository.MaxResourceViewCount} reached."); + } + return _resourceViews.GetOrAdd(view.Properties, view); } diff --git a/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs b/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs index 0d46579ccd3..bf0eee45d6b 100644 --- a/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs +++ b/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs @@ -26,6 +26,13 @@ namespace Aspire.Dashboard.Otlp.Storage; public sealed partial class TelemetryRepository : IDisposable { + internal const int MaxResourceViewCount = 10_000; + internal const int MaxInstrumentCount = 10_000; + internal const int MaxScopeCount = 10_000; + internal const int MaxDimensionCount = 10_000; + internal const int MaxKnownAttributeValueCount = 10_000; + internal const int MaxKnownAttributeValuesPerKey = 10_000; + private readonly PauseManager _pauseManager; private readonly IOutgoingPeerResolver[] _outgoingPeerResolvers; private readonly ILogger _logger; @@ -46,15 +53,21 @@ public sealed partial class TelemetryRepository : IDisposable private readonly ConcurrentDictionary _resources = new(); private readonly ReaderWriterLockSlim _logsLock = new(); + // Bounded by MaxScopeCount. Cleared when all logs are cleared. private readonly Dictionary _logScopes = new(); private readonly CircularBuffer _logs; + // Bounded by _resources count * MaxAttributeCount. Cleared per-resource or when all logs are cleared. private readonly HashSet<(OtlpResource Resource, string PropertyKey)> _logPropertyKeys = new(); + // Bounded by _resources count * MaxAttributeCount. Cleared per-resource or when all traces are cleared. private readonly HashSet<(OtlpResource Resource, string PropertyKey)> _tracePropertyKeys = new(); private readonly Dictionary _resourceUnviewedErrorLogs = new(); private readonly ReaderWriterLockSlim _tracesLock = new(); + // Bounded by MaxScopeCount. Cleared when all traces are cleared. private readonly Dictionary _traceScopes = new(); private readonly CircularBuffer _traces; + // Not explicitly capped per add — bounded only by the sum of span links across in-buffer traces. + // Cleaned up on trace eviction and clear, so growth is limited by the circular buffer capacity. private readonly List _spanLinks = new(); private readonly List _peerResolverSubscriptions = new(); internal readonly OtlpContext _otlpContext; @@ -237,6 +250,14 @@ private OtlpResourceView GetOrAddResourceView(Resource resource) return (Resource: resource, IsNew: false); } + // Check resource limit before adding a new resource. + // Note: This is a soft cap. Concurrent callers may both pass this check and slightly exceed the limit + // because _resources is a ConcurrentDictionary and the count check + GetOrAdd are not atomic. + if (_resources.Count >= _otlpContext.Options.MaxResourceCount) + { + throw new InvalidOperationException($"Resource limit of {_otlpContext.Options.MaxResourceCount} reached. Resource '{key}' will not be added."); + } + // Slower get or add path. // This GetOrAdd allocates a closure, so we avoid it if possible. var newResource = false; @@ -323,7 +344,7 @@ public void AddLogs(AddContext context, RepeatedField resourceLogs } catch (Exception ex) { - context.FailureCount += rl.ScopeLogs.Count; + context.FailureCount += rl.ScopeLogs.Sum(s => s.LogRecords.Count); _otlpContext.Logger.LogInformation(ex, "Error adding resource."); continue; } @@ -792,22 +813,41 @@ public void ClearTraces(ResourceKey? resourceKey = null) { // Nothing selected, clear everything. _traces.Clear(); + _traceScopes.Clear(); + _tracePropertyKeys.Clear(); + _spanLinks.Clear(); + + foreach (var resource in _resources.Values) + { + SetResourceHasTraces(resource, false); + } } else { for (var i = _traces.Count - 1; i >= 0; i--) { + var trace = _traces[i]; // Remove trace if any span matches one of the resources. This matches filter behavior. - if (MatchResources(_traces[i], resources)) + if (MatchResources(trace, resources)) { + // Remove span links for the removed trace. + foreach (var span in trace.Spans) + { + foreach (var link in span.Links) + { + _spanLinks.Remove(link); + } + } + _traces.RemoveAt(i); continue; } } - // Update HasTraces flag for cleared resources + // Remove property keys for cleared resources. foreach (var resource in resources) { + _tracePropertyKeys.RemoveWhere(k => k.Resource.ResourceKey == resource.ResourceKey); SetResourceHasTraces(resource, false); } } @@ -836,6 +876,15 @@ public void ClearStructuredLogs(ResourceKey? resourceKey = null) { // Nothing selected, clear everything. _logs.Clear(); + _logScopes.Clear(); + _logPropertyKeys.Clear(); + + foreach (var resource in _resources.Values) + { + SetResourceHasLogs(resource, false); + } + + _resourceUnviewedErrorLogs.Clear(); } else { @@ -848,9 +897,10 @@ public void ClearStructuredLogs(ResourceKey? resourceKey = null) } } - // Update HasLogs flag for cleared resources + // Update HasLogs flag and remove property keys for cleared resources. foreach (var resource in resources) { + _logPropertyKeys.RemoveWhere(k => k.Resource.ResourceKey == resource.ResourceKey); SetResourceHasLogs(resource, false); _resourceUnviewedErrorLogs.Remove(resource.ResourceKey); } @@ -1066,7 +1116,7 @@ public void AddMetrics(AddContext context, RepeatedField resour } catch (Exception ex) { - context.FailureCount += rm.ScopeMetrics.Sum(s => s.Metrics.Count); + context.FailureCount += rm.ScopeMetrics.Sum(sm => sm.Metrics.Sum(OtlpResource.GetMetricDataPointCount)); _otlpContext.Logger.LogInformation(ex, "Error adding resource."); continue; } @@ -1316,9 +1366,17 @@ static bool TryGetTraceById(CircularBuffer traces, ReadOnlyMemory + { + new ResourceSpans + { + Resource = CreateResource(name: $"app{i}"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = { CreateSpan("trace1", $"span{i}", s_testTime, s_testTime.AddMinutes(1)) } + } + } + } + }); + Assert.Equal(0, addContext.FailureCount); + } + + Assert.Equal(3, repository.GetResources().Count); + + // Adding a 4th resource should fail. + var failContext = new AddContext(); + repository.AddTraces(failContext, new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(name: "app-over-limit"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = { CreateSpan("trace2", "spanX", s_testTime, s_testTime.AddMinutes(1)) } + } + } + } + }); + + Assert.Equal(1, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + Assert.Equal(3, repository.GetResources().Count); + } + + [Fact] + public void AddTraces_ExistingResourceAfterLimitReached_Succeeds() + { + var repository = CreateRepository(maxResourceCount: 2); + + // Add 2 resources to fill up the limit. + for (var i = 0; i < 2; i++) + { + var addContext = new AddContext(); + repository.AddTraces(addContext, new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(name: $"app{i}"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = { CreateSpan("trace1", $"span{i}", s_testTime, s_testTime.AddMinutes(1)) } + } + } + } + }); + Assert.Equal(0, addContext.FailureCount); + } + + // Adding data for an existing resource should still succeed. + var successContext = new AddContext(); + repository.AddTraces(successContext, new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(name: "app0"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = { CreateSpan("trace2", "spanNew", s_testTime, s_testTime.AddMinutes(2)) } + } + } + } + }); + + Assert.Equal(0, successContext.FailureCount); + Assert.Equal(1, successContext.SuccessCount); + } + + [Fact] + public void AddMetrics_ExceedsInstrumentLimit_ReportsFailure() + { + var repository = CreateRepository(); + + // Fill instruments up to the limit. + var metrics = new RepeatedField(); + for (var i = 0; i < TelemetryRepository.MaxInstrumentCount; i++) + { + metrics.Add(CreateSumMetric(metricName: $"metric{i}", startTime: s_testTime.AddMinutes(1))); + } + + var addContext = new AddContext(); + repository.AddMetrics(addContext, new RepeatedField + { + new ResourceMetrics + { + Resource = CreateResource(), + ScopeMetrics = + { + new ScopeMetrics + { + Scope = CreateScope(name: "test-meter"), + Metrics = { metrics } + } + } + } + }); + + Assert.Equal(0, addContext.FailureCount); + + var resources = repository.GetResources(); + var instruments = repository.GetInstrumentsSummaries(resources[0].ResourceKey); + Assert.Equal(TelemetryRepository.MaxInstrumentCount, instruments.Count); + + // Adding one more instrument should fail. + var failContext = new AddContext(); + repository.AddMetrics(failContext, new RepeatedField + { + new ResourceMetrics + { + Resource = CreateResource(), + ScopeMetrics = + { + new ScopeMetrics + { + Scope = CreateScope(name: "test-meter"), + Metrics = { CreateSumMetric(metricName: "over-limit-metric", startTime: s_testTime.AddMinutes(2)) } + } + } + } + }); + + Assert.Equal(1, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + + instruments = repository.GetInstrumentsSummaries(resources[0].ResourceKey); + Assert.Equal(TelemetryRepository.MaxInstrumentCount, instruments.Count); + } + + [Fact] + public void AddLogs_ExceedsResourceLimit_FailureCountIsLogRecordCount() + { + var repository = CreateRepository(maxResourceCount: 1); + + // Fill the single resource slot. + var setupContext = new AddContext(); + repository.AddLogs(setupContext, new RepeatedField + { + new ResourceLogs + { + Resource = CreateResource(name: "app0"), + ScopeLogs = + { + new ScopeLogs + { + Scope = CreateScope("logger"), + LogRecords = { CreateLogRecord() } + } + } + } + }); + Assert.Equal(0, setupContext.FailureCount); + + // Attempt to add logs for a new resource with multiple scopes and records. + // FailureCount must equal total log records, not number of scopes. + var failContext = new AddContext(); + repository.AddLogs(failContext, new RepeatedField + { + new ResourceLogs + { + Resource = CreateResource(name: "app-over-limit"), + ScopeLogs = + { + new ScopeLogs + { + Scope = CreateScope("loggerA"), + LogRecords = + { + CreateLogRecord(message: "a1"), + CreateLogRecord(message: "a2"), + CreateLogRecord(message: "a3") + } + }, + new ScopeLogs + { + Scope = CreateScope("loggerB"), + LogRecords = + { + CreateLogRecord(message: "b1"), + CreateLogRecord(message: "b2") + } + } + } + } + }); + + Assert.Equal(5, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + } + + [Fact] + public void AddMetrics_ExceedsResourceLimit_FailureCountIsDataPointCount() + { + var repository = CreateRepository(maxResourceCount: 1); + + // Fill the single resource slot. + var setupContext = new AddContext(); + repository.AddMetrics(setupContext, new RepeatedField + { + new ResourceMetrics + { + Resource = CreateResource(name: "app0"), + ScopeMetrics = + { + new ScopeMetrics + { + Scope = CreateScope(name: "meter"), + Metrics = { CreateSumMetric(metricName: "m0", startTime: s_testTime.AddMinutes(1)) } + } + } + } + }); + Assert.Equal(0, setupContext.FailureCount); + + // Attempt to add metrics for a new resource with multiple scopes and metrics. + // FailureCount must equal total data points, not number of metrics. + var failContext = new AddContext(); + repository.AddMetrics(failContext, new RepeatedField + { + new ResourceMetrics + { + Resource = CreateResource(name: "app-over-limit"), + ScopeMetrics = + { + new ScopeMetrics + { + Scope = CreateScope(name: "meterA"), + Metrics = + { + CreateSumMetric(metricName: "m1", startTime: s_testTime.AddMinutes(1)), + CreateSumMetric(metricName: "m2", startTime: s_testTime.AddMinutes(1)), + CreateSumMetric(metricName: "m3", startTime: s_testTime.AddMinutes(1)) + } + }, + new ScopeMetrics + { + Scope = CreateScope(name: "meterB"), + Metrics = + { + CreateSumMetric(metricName: "m4", startTime: s_testTime.AddMinutes(1)), + CreateSumMetric(metricName: "m5", startTime: s_testTime.AddMinutes(1)) + } + } + } + } + }); + + // Each CreateSumMetric produces 1 data point, so 5 metrics = 5 data points. + Assert.Equal(5, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + } + + [Fact] + public void AddTraces_ExceedsResourceLimit_FailureCountIsSpanCount() + { + var repository = CreateRepository(maxResourceCount: 1); + + // Fill the single resource slot. + var setupContext = new AddContext(); + repository.AddTraces(setupContext, new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(name: "app0"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = { CreateSpan("trace1", "span0", s_testTime, s_testTime.AddMinutes(1)) } + } + } + } + }); + Assert.Equal(0, setupContext.FailureCount); + + // Attempt to add traces for a new resource with multiple scopes and spans. + // FailureCount must equal total spans, not number of scopes. + var failContext = new AddContext(); + repository.AddTraces(failContext, new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(name: "app-over-limit"), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(), + Spans = + { + CreateSpan("trace2", "spanA1", s_testTime, s_testTime.AddMinutes(1)), + CreateSpan("trace2", "spanA2", s_testTime, s_testTime.AddMinutes(1)) + } + }, + new ScopeSpans + { + Scope = CreateScope(), + Spans = + { + CreateSpan("trace3", "spanB1", s_testTime, s_testTime.AddMinutes(1)) + } + } + } + } + }); + + Assert.Equal(3, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + } + + [Fact] + public void AddLogs_ExceedsScopeLimit_ReportsFailure() + { + var repository = CreateRepository(); + + // Fill scopes up to the limit. + var scopeLogs = new RepeatedField(); + var rl = new ResourceLogs { Resource = CreateResource() }; + for (var i = 0; i < TelemetryRepository.MaxScopeCount; i++) + { + rl.ScopeLogs.Add(new ScopeLogs + { + Scope = CreateScope(name: $"logger{i}"), + LogRecords = { CreateLogRecord() } + }); + } + scopeLogs.Add(rl); + + var addContext = new AddContext(); + repository.AddLogs(addContext, scopeLogs); + Assert.Equal(0, addContext.FailureCount); + + // Adding one more scope should fail. + var failContext = new AddContext(); + repository.AddLogs(failContext, new RepeatedField + { + new ResourceLogs + { + Resource = CreateResource(), + ScopeLogs = + { + new ScopeLogs + { + Scope = CreateScope(name: "over-limit-logger"), + LogRecords = + { + CreateLogRecord(message: "a"), + CreateLogRecord(message: "b"), + CreateLogRecord(message: "c") + } + } + } + } + }); + + Assert.Equal(3, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + } + + [Fact] + public void AddTraces_ExceedsScopeLimit_ReportsFailure() + { + var repository = CreateRepository(); + + // Fill scopes up to the limit. + var rs = new ResourceSpans { Resource = CreateResource() }; + for (var i = 0; i < TelemetryRepository.MaxScopeCount; i++) + { + rs.ScopeSpans.Add(new ScopeSpans + { + Scope = CreateScope(name: $"tracer{i}"), + Spans = { CreateSpan($"trace{i}", $"span{i}", s_testTime, s_testTime.AddMinutes(1)) } + }); + } + + var addContext = new AddContext(); + repository.AddTraces(addContext, new RepeatedField { rs }); + Assert.Equal(0, addContext.FailureCount); + + // Adding one more scope should fail. + var failContext = new AddContext(); + repository.AddTraces(failContext, new RepeatedField + { + new ResourceSpans + { + Resource = CreateResource(), + ScopeSpans = + { + new ScopeSpans + { + Scope = CreateScope(name: "over-limit-tracer"), + Spans = + { + CreateSpan("traceX", "spanX1", s_testTime, s_testTime.AddMinutes(1)), + CreateSpan("traceX", "spanX2", s_testTime, s_testTime.AddMinutes(2)) + } + } + } + } + }); + + Assert.Equal(2, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + } + + [Fact] + public void AddMetrics_ExceedsScopeLimit_ReportsFailure() + { + var repository = CreateRepository(); + + // Fill scopes up to the limit. + var rm = new ResourceMetrics { Resource = CreateResource() }; + for (var i = 0; i < TelemetryRepository.MaxScopeCount; i++) + { + rm.ScopeMetrics.Add(new ScopeMetrics + { + Scope = CreateScope(name: $"meter{i}"), + Metrics = { CreateSumMetric(metricName: $"metric{i}", startTime: s_testTime.AddMinutes(1)) } + }); + } + + var addContext = new AddContext(); + repository.AddMetrics(addContext, new RepeatedField { rm }); + Assert.Equal(0, addContext.FailureCount); + + // Adding one more scope should fail. Each metric has 1 data point. + var failContext = new AddContext(); + repository.AddMetrics(failContext, new RepeatedField + { + new ResourceMetrics + { + Resource = CreateResource(), + ScopeMetrics = + { + new ScopeMetrics + { + Scope = CreateScope(name: "over-limit-meter"), + Metrics = + { + CreateSumMetric(metricName: "m1", startTime: s_testTime.AddMinutes(1)), + CreateSumMetric(metricName: "m2", startTime: s_testTime.AddMinutes(1)) + } + } + } + } + }); + + // 2 metrics × 1 data point each = 2 rejected data points. + Assert.Equal(2, failContext.FailureCount); + Assert.Equal(0, failContext.SuccessCount); + } +} diff --git a/tests/Shared/Telemetry/TelemetryTestHelpers.cs b/tests/Shared/Telemetry/TelemetryTestHelpers.cs index 16dcad85bd6..0b559ccb215 100644 --- a/tests/Shared/Telemetry/TelemetryTestHelpers.cs +++ b/tests/Shared/Telemetry/TelemetryTestHelpers.cs @@ -238,6 +238,7 @@ public static TelemetryRepository CreateRepository( int? maxSpanEventCount = null, int? maxTraceCount = null, int? maxLogCount = null, + int? maxResourceCount = null, TimeSpan? subscriptionMinExecuteInterval = null, ILoggerFactory? loggerFactory = null, PauseManager? pauseManager = null, @@ -268,6 +269,10 @@ public static TelemetryRepository CreateRepository( { options.MaxLogCount = maxLogCount.Value; } + if (maxResourceCount != null) + { + options.MaxResourceCount = maxResourceCount.Value; + } var repository = new TelemetryRepository( loggerFactory ?? NullLoggerFactory.Instance, From cf62314a750f83b4c4b54660efc689b639772448 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 11:42:03 -0700 Subject: [PATCH 14/55] Move Ingress and Gateway extension methods to Aspire.Hosting namespace (#16633) KubernetesGatewayExtensions and KubernetesIngressExtensions were in the Aspire.Hosting.Kubernetes namespace, requiring users to add an explicit using directive. Move them to Aspire.Hosting to match the convention used by other extension methods like KubernetesEnvironmentExtensions and KubernetesServiceExtensions. Co-authored-by: Mitch Denny Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting.Kubernetes/KubernetesGatewayExtensions.cs | 3 ++- src/Aspire.Hosting.Kubernetes/KubernetesIngressExtensions.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesGatewayExtensions.cs b/src/Aspire.Hosting.Kubernetes/KubernetesGatewayExtensions.cs index 489ef7a27f1..6d7df8a3cfd 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesGatewayExtensions.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesGatewayExtensions.cs @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Kubernetes; -namespace Aspire.Hosting.Kubernetes; +namespace Aspire.Hosting; /// /// Provides extension methods for configuring Kubernetes Gateway API resources in the Aspire application model. diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesIngressExtensions.cs b/src/Aspire.Hosting.Kubernetes/KubernetesIngressExtensions.cs index c7c1cbe7b11..25e2fceb3ae 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesIngressExtensions.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesIngressExtensions.cs @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Kubernetes; -namespace Aspire.Hosting.Kubernetes; +namespace Aspire.Hosting; /// /// Provides extension methods for configuring Kubernetes Ingress resources in the Aspire application model. From 6b4a7ccb3d9aac53c30f3e631a6bc9a5bfde8508 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 11:50:01 -0700 Subject: [PATCH 15/55] Remove obsolete ATS export shims (#16628) Remove obsolete internal compatibility shims from the ATS export surface so generated polyglot SDKs only expose the unified methods. Co-authored-by: Sebastien Ros Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureBicepResourceExtensions.cs | 35 - .../api/Aspire.Hosting.SqlServer.ats.txt | 4 - .../IYarpConfigurationBuilder.cs | 14 - .../ResourceBuilderExtensions.cs | 70 -- .../api/Aspire.Hosting.Capabilities.txt | 4 - ...TwoPassScanningGeneratedAspire.verified.go | 448 ------- ...oPassScanningGeneratedAspire.verified.java | 352 ------ ...TwoPassScanningGeneratedAspire.verified.py | 268 ----- ...TwoPassScanningGeneratedAspire.verified.rs | 352 ------ .../AtsTypeScriptCodeGeneratorTests.cs | 12 - ...ContainerResourceCapabilities.verified.txt | 56 - ...TwoPassScanningGeneratedAspire.verified.ts | 1035 ----------------- 12 files changed, 2650 deletions(-) diff --git a/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs b/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs index c8fbac520bd..13072f0d32a 100644 --- a/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs @@ -85,25 +85,6 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu return builder.WithEnvironment(name, (IExpressionValue)bicepOutputReference); } - // Keep these ATS-only aliases for backward compatibility with existing polyglot app hosts. - // Remove them once callers have migrated to the unified withEnvironment(...) export. - // Tracking issue: https://github.com/microsoft/aspire/issues/15734 - /// - /// Obsolete ATS alias for . - /// - /// The resource type. - /// The resource builder. - /// The name of the environment variable. - /// The reference to the Bicep output. - /// An . - [Obsolete("ATS compatibility shim. Use withEnvironment instead.")] - [AspireExport("withEnvironmentFromOutput", Description = "Sets an environment variable from a Bicep output reference")] - internal static IResourceBuilder WithEnvironmentFromOutputShim(this IResourceBuilder builder, string name, BicepOutputReference bicepOutputReference) - where T : IResourceWithEnvironment - { - return builder.WithEnvironment(name, bicepOutputReference); - } - /// /// Adds an environment variable to the resource with the value of the secret output from the bicep template. /// @@ -137,22 +118,6 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu return builder.WithEnvironment(name, (IExpressionValue)secretReference); } - /// - /// Obsolete ATS alias for . - /// - /// The resource type. - /// The resource builder. - /// The name of the environment variable. - /// The key vault secret reference. - /// An . - [Obsolete("ATS compatibility shim. Use withEnvironment instead.")] - [AspireExport("withEnvironmentFromKeyVaultSecret", Description = "Sets an environment variable from an Azure Key Vault secret reference")] - internal static IResourceBuilder WithEnvironmentFromKeyVaultSecretShim(this IResourceBuilder builder, string name, IAzureKeyVaultSecretReference secretReference) - where T : IResourceWithEnvironment - { - return builder.WithEnvironment(name, secretReference); - } - /// /// Adds a parameter to the bicep template. /// diff --git a/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.ats.txt b/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.ats.txt index 7caf46bef63..ff309474bcc 100644 --- a/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.ats.txt +++ b/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.ats.txt @@ -418,10 +418,6 @@ Aspire.Hosting/withEndpointProxySupport(proxyEnabled: boolean) -> Aspire.Hosting Aspire.Hosting/withEntrypoint(entrypoint: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withEnvironment(name: string, value: string|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression|Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithConnectionString|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExpressionValue) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withEnvironmentCallback(callback: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentConnectionString(envVarName: string, resource: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithConnectionString) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentEndpoint(name: string, endpointReference: Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentExpression(name: string, value: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentParameter(name: string, parameter: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withExecutableCommand(command: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecutableResource Aspire.Hosting/withExplicitStart() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withExternalHttpEndpoints() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints diff --git a/src/Aspire.Hosting.Yarp/ConfigurationBuilder/IYarpConfigurationBuilder.cs b/src/Aspire.Hosting.Yarp/ConfigurationBuilder/IYarpConfigurationBuilder.cs index e651459f1ae..d319b299f0d 100644 --- a/src/Aspire.Hosting.Yarp/ConfigurationBuilder/IYarpConfigurationBuilder.cs +++ b/src/Aspire.Hosting.Yarp/ConfigurationBuilder/IYarpConfigurationBuilder.cs @@ -251,20 +251,6 @@ internal static YarpRoute AddRoute( return AddRouteCore(builder, path, target); } - /// - /// Adds a route for an existing cluster. - /// - /// The builder instance. - /// The path to match for this route. - /// The target cluster for this route. - /// The created route. - [Obsolete("Use addRoute(path, target) instead.")] - [AspireExport("IYarpConfigurationBuilder.addRoute", MethodName = "addRouteCluster", Description = "Obsolete compatibility shim for the previous cluster-only addRoute export. Use addRoute(path, target) instead.")] - internal static YarpRoute AddRouteCluster(this IYarpConfigurationBuilder builder, string path, YarpCluster cluster) - { - return builder.AddRoute(path, cluster); - } - /// /// Add a new catch all route to YARP that will target the cluster in parameter. /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index d4d138868e3..8d4f75dcc8f 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -141,25 +141,6 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu }); } - // Keep these ATS-only aliases for backward compatibility with existing polyglot app hosts. - // Remove them once callers have migrated to the unified withEnvironment(...) export. - // Tracking issue: https://github.com/microsoft/aspire/issues/15734 - /// - /// Obsolete ATS alias for . - /// - /// The resource type. - /// The resource builder. - /// The name of the environment variable. - /// The reference expression value. - /// The . - [Obsolete("ATS compatibility shim. Use withEnvironment instead.")] - [AspireExport("withEnvironmentExpression", Description = "Sets an environment variable from a reference expression")] - internal static IResourceBuilder WithEnvironmentExpressionShim(this IResourceBuilder builder, string name, ReferenceExpression value) - where T : IResourceWithEnvironment - { - return builder.WithEnvironment(name, value); - } - /// /// Adds an environment variable to the resource. /// @@ -235,22 +216,6 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu }); } - /// - /// Obsolete ATS alias for . - /// - /// The resource type. - /// The resource builder. - /// The name of the environment variable. - /// The endpoint reference value. - /// The . - [Obsolete("ATS compatibility shim. Use withEnvironment instead.")] - [AspireExport("withEnvironmentEndpoint", Description = "Sets an environment variable from an endpoint reference")] - internal static IResourceBuilder WithEnvironmentEndpointShim(this IResourceBuilder builder, string name, EndpointReference endpointReference) - where T : IResourceWithEnvironment - { - return builder.WithEnvironment(name, endpointReference); - } - /// /// Adds an environment variable to the resource with the URL from the . /// @@ -317,22 +282,6 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu }); } - /// - /// Obsolete ATS alias for . - /// - /// The resource type. - /// The resource builder. - /// The name of the environment variable. - /// The parameter resource builder. - /// The . - [Obsolete("ATS compatibility shim. Use withEnvironment instead.")] - [AspireExport("withEnvironmentParameter", Description = "Sets an environment variable from a parameter resource")] - internal static IResourceBuilder WithEnvironmentParameterShim(this IResourceBuilder builder, string name, IResourceBuilder parameter) - where T : IResourceWithEnvironment - { - return builder.WithEnvironment(name, parameter); - } - /// /// Adds an environment variable to the resource with the connection string from the referenced resource. /// @@ -360,25 +309,6 @@ public static IResourceBuilder WithEnvironment( }); } - /// - /// Obsolete ATS alias for . - /// - /// The destination resource type. - /// The destination resource builder. - /// The name of the environment variable. - /// The referenced connection string resource builder. - /// The . - [Obsolete("ATS compatibility shim. Use withEnvironment instead.")] - [AspireExport("withEnvironmentConnectionString", Description = "Sets an environment variable from a connection string resource")] - internal static IResourceBuilder WithEnvironmentConnectionStringShim( - this IResourceBuilder builder, - string envVarName, - IResourceBuilder resource) - where T : IResourceWithEnvironment - { - return builder.WithEnvironment(envVarName, resource); - } - /// /// Adds an environment variable to the resource with a value that provides both a runtime value and a manifest expression. /// diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.Capabilities.txt b/src/Aspire.Hosting/api/Aspire.Hosting.Capabilities.txt index 1708174226c..eb49d9ba830 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.Capabilities.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.Capabilities.txt @@ -388,10 +388,6 @@ Aspire.Hosting/withEndpointProxySupport(proxyEnabled: boolean) -> Aspire.Hosting Aspire.Hosting/withEntrypoint(entrypoint: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withEnvironment(name: string, value: string|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression|Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithConnectionString|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExpressionValue) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withEnvironmentCallback(callback: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentConnectionString(envVarName: string, resource: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithConnectionString) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentEndpoint(name: string, endpointReference: Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentExpression(name: string, value: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment -Aspire.Hosting/withEnvironmentParameter(name: string, parameter: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withExecutableCommand(command: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecutableResource Aspire.Hosting/withExplicitStart() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withExternalHttpEndpoints() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index f91efadd206..c76b837de70 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -882,20 +882,6 @@ func (s *CSharpAppResource) WithEnvironment(name string, value any) (*IResourceW return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *CSharpAppResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *CSharpAppResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -911,48 +897,6 @@ func (s *CSharpAppResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *CSharpAppResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *CSharpAppResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *CSharpAppResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithArgs adds arguments func (s *CSharpAppResource) WithArgs(args []string) (*IResourceWithArgs, error) { reqArgs := map[string]any{ @@ -3480,20 +3424,6 @@ func (s *ContainerResource) WithEnvironment(name string, value any) (*IResourceW return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *ContainerResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *ContainerResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -3509,48 +3439,6 @@ func (s *ContainerResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *ContainerResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *ContainerResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *ContainerResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithArgs adds arguments func (s *ContainerResource) WithArgs(args []string) (*IResourceWithArgs, error) { reqArgs := map[string]any{ @@ -5454,20 +5342,6 @@ func (s *DotnetToolResource) WithEnvironment(name string, value any) (*IResource return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *DotnetToolResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *DotnetToolResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -5483,48 +5357,6 @@ func (s *DotnetToolResource) WithEnvironmentCallback(callback func(...any) any) return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *DotnetToolResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *DotnetToolResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *DotnetToolResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithArgs adds arguments func (s *DotnetToolResource) WithArgs(args []string) (*IResourceWithArgs, error) { reqArgs := map[string]any{ @@ -7539,20 +7371,6 @@ func (s *ExecutableResource) WithEnvironment(name string, value any) (*IResource return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *ExecutableResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *ExecutableResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -7568,48 +7386,6 @@ func (s *ExecutableResource) WithEnvironmentCallback(callback func(...any) any) return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *ExecutableResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *ExecutableResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *ExecutableResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithArgs adds arguments func (s *ExecutableResource) WithArgs(args []string) (*IResourceWithArgs, error) { reqArgs := map[string]any{ @@ -12442,20 +12218,6 @@ func (s *ProjectResource) WithEnvironment(name string, value any) (*IResourceWit return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *ProjectResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *ProjectResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -12471,48 +12233,6 @@ func (s *ProjectResource) WithEnvironmentCallback(callback func(...any) any) (*I return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *ProjectResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *ProjectResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *ProjectResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithArgs adds arguments func (s *ProjectResource) WithArgs(args []string) (*IResourceWithArgs, error) { reqArgs := map[string]any{ @@ -14689,20 +14409,6 @@ func (s *TestDatabaseResource) WithEnvironment(name string, value any) (*IResour return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *TestDatabaseResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *TestDatabaseResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -14718,48 +14424,6 @@ func (s *TestDatabaseResource) WithEnvironmentCallback(callback func(...any) any return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *TestDatabaseResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *TestDatabaseResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *TestDatabaseResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithArgs adds arguments func (s *TestDatabaseResource) WithArgs(args []string) (*IResourceWithArgs, error) { reqArgs := map[string]any{ @@ -16453,20 +16117,6 @@ func (s *TestRedisResource) WithEnvironment(name string, value any) (*IResourceW return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *TestRedisResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *TestRedisResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -16482,48 +16132,6 @@ func (s *TestRedisResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *TestRedisResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *TestRedisResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *TestRedisResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithConnectionProperty adds a connection property with a string or reference expression value func (s *TestRedisResource) WithConnectionProperty(name string, value any) (*IResourceWithConnectionString, error) { reqArgs := map[string]any{ @@ -18372,20 +17980,6 @@ func (s *TestVaultResource) WithEnvironment(name string, value any) (*IResourceW return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentExpression sets an environment variable from a reference expression -func (s *TestVaultResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *TestVaultResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -18401,48 +17995,6 @@ func (s *TestVaultResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *TestVaultResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentParameter sets an environment variable from a parameter resource -func (s *TestVaultResource) WithEnvironmentParameter(name string, parameter *ParameterResource) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["parameter"] = SerializeValue(parameter) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentConnectionString sets an environment variable from a connection string resource -func (s *TestVaultResource) WithEnvironmentConnectionString(envVarName string, resource *IResourceWithConnectionString) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["envVarName"] = SerializeValue(envVarName) - reqArgs["resource"] = SerializeValue(resource) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithArgs adds arguments func (s *TestVaultResource) WithArgs(args []string) (*IResourceWithArgs, error) { reqArgs := map[string]any{ diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index dc10ced1f7e..e4fde40e103 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1668,16 +1668,6 @@ public CSharpAppResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public CSharpAppResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public CSharpAppResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -1694,40 +1684,6 @@ public CSharpAppResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public CSharpAppResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public CSharpAppResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public CSharpAppResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - /** Adds arguments */ public CSharpAppResource withArgs(String[] args) { Map reqArgs = new HashMap<>(); @@ -4771,16 +4727,6 @@ public ContainerResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public ContainerResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public ContainerResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -4797,40 +4743,6 @@ public ContainerResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public ContainerResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public ContainerResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public ContainerResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - /** Adds arguments */ public ContainerResource withArgs(String[] args) { Map reqArgs = new HashMap<>(); @@ -6906,16 +6818,6 @@ public DotnetToolResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public DotnetToolResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public DotnetToolResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -6932,40 +6834,6 @@ public DotnetToolResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public DotnetToolResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public DotnetToolResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public DotnetToolResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - /** Adds arguments */ public DotnetToolResource withArgs(String[] args) { Map reqArgs = new HashMap<>(); @@ -8966,16 +8834,6 @@ public ExecutableResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public ExecutableResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public ExecutableResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -8992,40 +8850,6 @@ public ExecutableResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public ExecutableResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public ExecutableResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public ExecutableResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - /** Adds arguments */ public ExecutableResource withArgs(String[] args) { Map reqArgs = new HashMap<>(); @@ -14342,16 +14166,6 @@ public ProjectResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public ProjectResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public ProjectResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -14368,40 +14182,6 @@ public ProjectResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public ProjectResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public ProjectResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public ProjectResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - /** Adds arguments */ public ProjectResource withArgs(String[] args) { Map reqArgs = new HashMap<>(); @@ -17030,16 +16810,6 @@ public TestDatabaseResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public TestDatabaseResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public TestDatabaseResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -17056,40 +16826,6 @@ public TestDatabaseResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public TestDatabaseResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public TestDatabaseResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public TestDatabaseResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - /** Adds arguments */ public TestDatabaseResource withArgs(String[] args) { Map reqArgs = new HashMap<>(); @@ -18963,16 +18699,6 @@ public TestRedisResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public TestRedisResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public TestRedisResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -18989,40 +18715,6 @@ public TestRedisResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public TestRedisResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public TestRedisResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public TestRedisResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - public TestRedisResource withConnectionProperty(String name, String value) { return withConnectionProperty(name, AspireUnion.of(value)); } @@ -20989,16 +20681,6 @@ public TestVaultResource withEnvironment(String name, AspireUnion value) { return this; } - /** Sets an environment variable from a reference expression */ - public TestVaultResource withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - return this; - } - /** Sets environment variables via callback */ public TestVaultResource withEnvironmentCallback(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -21015,40 +20697,6 @@ public TestVaultResource withEnvironmentCallback(AspireAction1 reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("endpointReference", AspireClient.serializeValue(endpointReference)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); - return this; - } - - /** Sets an environment variable from a parameter resource */ - public TestVaultResource withEnvironmentParameter(String name, ParameterResource parameter) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("parameter", AspireClient.serializeValue(parameter)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentParameter", reqArgs); - return this; - } - - /** Sets an environment variable from a connection string resource */ - public TestVaultResource withEnvironmentConnectionString(String envVarName, IResourceWithConnectionString resource) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("envVarName", AspireClient.serializeValue(envVarName)); - reqArgs.put("resource", AspireClient.serializeValue(resource)); - getClient().invokeCapability("Aspire.Hosting/withEnvironmentConnectionString", reqArgs); - return this; - } - - public TestVaultResource withEnvironmentConnectionString(String envVarName, ResourceBuilderBase resource) { - return withEnvironmentConnectionString(envVarName, new IResourceWithConnectionString(resource.getHandle(), resource.getClient())); - } - /** Adds arguments */ public TestVaultResource withArgs(String[] args) { Map reqArgs = new HashMap<>(); diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 825d27fadba..5f5248f6e38 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -5953,22 +5953,6 @@ def with_env(self, name: str, value: str | ReferenceExpression | EndpointReferen def with_env_callback(self, callback: typing.Callable[[EnvironmentCallbackContext], None]) -> typing.Self: """Sets environment variables via callback""" - @abc.abstractmethod - def with_env_expression(self, name: str, value: ReferenceExpression) -> typing.Self: - """Sets an environment variable from a reference expression""" - - @abc.abstractmethod - def with_env_endpoint(self, name: str, endpoint_reference: EndpointReference) -> typing.Self: - """Sets an environment variable from an endpoint reference""" - - @abc.abstractmethod - def with_env_parameter(self, name: str, parameter: ParameterResource) -> typing.Self: - """Sets an environment variable from a parameter resource""" - - @abc.abstractmethod - def with_env_connection_string(self, env_var_name: str, resource: AbstractResourceWithConnectionString) -> typing.Self: - """Sets an environment variable from a connection string resource""" - @abc.abstractmethod def with_reference_env(self, options: ReferenceEnvironmentInjectionOptions) -> typing.Self: """Configures which reference values are injected into environment variables""" @@ -6977,10 +6961,6 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): publish_as_connection_string: typing.Literal[True] env: tuple[str, str | ReferenceExpression | EndpointReference | ParameterResource | AbstractResourceWithConnectionString | AbstractExpressionValue] env_callback: typing.Callable[[EnvironmentCallbackContext], None] - env_expression: tuple[str, ReferenceExpression] - env_endpoint: tuple[str, EndpointReference] - env_parameter: tuple[str, ParameterResource] - env_connection_string: tuple[str, AbstractResourceWithConnectionString] args: typing.Iterable[str] args_callback: typing.Callable[[CommandLineArgsCallbackContext], None] reference_env: ReferenceEnvironmentInjectionOptions @@ -7308,54 +7288,6 @@ def with_env_callback(self, callback: typing.Callable[[EnvironmentCallbackContex self._handle = self._wrap_builder(result) return self - def with_env_expression(self, name: str, value: ReferenceExpression) -> typing.Self: - """Sets an environment variable from a reference expression""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['value'] = value - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentExpression', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_endpoint(self, name: str, endpoint_reference: EndpointReference) -> typing.Self: - """Sets an environment variable from an endpoint reference""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['endpointReference'] = endpoint_reference - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_parameter(self, name: str, parameter: ParameterResource) -> typing.Self: - """Sets an environment variable from a parameter resource""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['parameter'] = parameter - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentParameter', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_connection_string(self, env_var_name: str, resource: AbstractResourceWithConnectionString) -> typing.Self: - """Sets an environment variable from a connection string resource""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['envVarName'] = env_var_name - rpc_args['resource'] = resource - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_args(self, args: typing.Iterable[str]) -> typing.Self: """Adds arguments""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7977,38 +7909,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentCallback', rpc_args)) else: raise TypeError("Invalid type for option 'env_callback'. Expected: Callable[[EnvironmentCallbackContext], None]") - if _env_expression := kwargs.pop("env_expression", None): - if _validate_tuple_types(_env_expression, (str, ReferenceExpression)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, ReferenceExpression], _env_expression)[0] - rpc_args["value"] = typing.cast(tuple[str, ReferenceExpression], _env_expression)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentExpression', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_expression'. Expected: (str, ReferenceExpression)") - if _env_endpoint := kwargs.pop("env_endpoint", None): - if _validate_tuple_types(_env_endpoint, (str, EndpointReference)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, EndpointReference], _env_endpoint)[0] - rpc_args["endpointReference"] = typing.cast(tuple[str, EndpointReference], _env_endpoint)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentEndpoint', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_endpoint'. Expected: (str, EndpointReference)") - if _env_parameter := kwargs.pop("env_parameter", None): - if _validate_tuple_types(_env_parameter, (str, ParameterResource)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, ParameterResource], _env_parameter)[0] - rpc_args["parameter"] = typing.cast(tuple[str, ParameterResource], _env_parameter)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentParameter', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_parameter'. Expected: (str, ParameterResource)") - if _env_connection_string := kwargs.pop("env_connection_string", None): - if _validate_tuple_types(_env_connection_string, (str, AbstractResourceWithConnectionString)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["envVarName"] = typing.cast(tuple[str, AbstractResourceWithConnectionString], _env_connection_string)[0] - rpc_args["resource"] = typing.cast(tuple[str, AbstractResourceWithConnectionString], _env_connection_string)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentConnectionString', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_connection_string'. Expected: (str, AbstractResourceWithConnectionString)") if _args := kwargs.pop("args", None): if _validate_type(_args, typing.Iterable[str]): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8320,10 +8220,6 @@ class ProjectResourceKwargs(_BaseResourceKwargs, total=False): publish_as_docker_file: typing.Callable[[ContainerResource], None] | typing.Literal[True] env: tuple[str, str | ReferenceExpression | EndpointReference | ParameterResource | AbstractResourceWithConnectionString | AbstractExpressionValue] env_callback: typing.Callable[[EnvironmentCallbackContext], None] - env_expression: tuple[str, ReferenceExpression] - env_endpoint: tuple[str, EndpointReference] - env_parameter: tuple[str, ParameterResource] - env_connection_string: tuple[str, AbstractResourceWithConnectionString] args: typing.Iterable[str] args_callback: typing.Callable[[CommandLineArgsCallbackContext], None] reference_env: ReferenceEnvironmentInjectionOptions @@ -8458,54 +8354,6 @@ def with_env_callback(self, callback: typing.Callable[[EnvironmentCallbackContex self._handle = self._wrap_builder(result) return self - def with_env_expression(self, name: str, value: ReferenceExpression) -> typing.Self: - """Sets an environment variable from a reference expression""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['value'] = value - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentExpression', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_endpoint(self, name: str, endpoint_reference: EndpointReference) -> typing.Self: - """Sets an environment variable from an endpoint reference""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['endpointReference'] = endpoint_reference - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_parameter(self, name: str, parameter: ParameterResource) -> typing.Self: - """Sets an environment variable from a parameter resource""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['parameter'] = parameter - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentParameter', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_connection_string(self, env_var_name: str, resource: AbstractResourceWithConnectionString) -> typing.Self: - """Sets an environment variable from a connection string resource""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['envVarName'] = env_var_name - rpc_args['resource'] = resource - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_args(self, args: typing.Iterable[str]) -> typing.Self: """Adds arguments""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -8984,38 +8832,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentCallback', rpc_args)) else: raise TypeError("Invalid type for option 'env_callback'. Expected: Callable[[EnvironmentCallbackContext], None]") - if _env_expression := kwargs.pop("env_expression", None): - if _validate_tuple_types(_env_expression, (str, ReferenceExpression)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, ReferenceExpression], _env_expression)[0] - rpc_args["value"] = typing.cast(tuple[str, ReferenceExpression], _env_expression)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentExpression', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_expression'. Expected: (str, ReferenceExpression)") - if _env_endpoint := kwargs.pop("env_endpoint", None): - if _validate_tuple_types(_env_endpoint, (str, EndpointReference)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, EndpointReference], _env_endpoint)[0] - rpc_args["endpointReference"] = typing.cast(tuple[str, EndpointReference], _env_endpoint)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentEndpoint', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_endpoint'. Expected: (str, EndpointReference)") - if _env_parameter := kwargs.pop("env_parameter", None): - if _validate_tuple_types(_env_parameter, (str, ParameterResource)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, ParameterResource], _env_parameter)[0] - rpc_args["parameter"] = typing.cast(tuple[str, ParameterResource], _env_parameter)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentParameter', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_parameter'. Expected: (str, ParameterResource)") - if _env_connection_string := kwargs.pop("env_connection_string", None): - if _validate_tuple_types(_env_connection_string, (str, AbstractResourceWithConnectionString)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["envVarName"] = typing.cast(tuple[str, AbstractResourceWithConnectionString], _env_connection_string)[0] - rpc_args["resource"] = typing.cast(tuple[str, AbstractResourceWithConnectionString], _env_connection_string)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentConnectionString', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_connection_string'. Expected: (str, AbstractResourceWithConnectionString)") if _args := kwargs.pop("args", None): if _validate_type(_args, typing.Iterable[str]): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9336,10 +9152,6 @@ class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): otlp_exporter: OtlpProtocol | typing.Literal[True] env: tuple[str, str | ReferenceExpression | EndpointReference | ParameterResource | AbstractResourceWithConnectionString | AbstractExpressionValue] env_callback: typing.Callable[[EnvironmentCallbackContext], None] - env_expression: tuple[str, ReferenceExpression] - env_endpoint: tuple[str, EndpointReference] - env_parameter: tuple[str, ParameterResource] - env_connection_string: tuple[str, AbstractResourceWithConnectionString] args: typing.Iterable[str] args_callback: typing.Callable[[CommandLineArgsCallbackContext], None] reference_env: ReferenceEnvironmentInjectionOptions @@ -9473,54 +9285,6 @@ def with_env_callback(self, callback: typing.Callable[[EnvironmentCallbackContex self._handle = self._wrap_builder(result) return self - def with_env_expression(self, name: str, value: ReferenceExpression) -> typing.Self: - """Sets an environment variable from a reference expression""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['value'] = value - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentExpression', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_endpoint(self, name: str, endpoint_reference: EndpointReference) -> typing.Self: - """Sets an environment variable from an endpoint reference""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['endpointReference'] = endpoint_reference - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_parameter(self, name: str, parameter: ParameterResource) -> typing.Self: - """Sets an environment variable from a parameter resource""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['name'] = name - rpc_args['parameter'] = parameter - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentParameter', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - - def with_env_connection_string(self, env_var_name: str, resource: AbstractResourceWithConnectionString) -> typing.Self: - """Sets an environment variable from a connection string resource""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['envVarName'] = env_var_name - rpc_args['resource'] = resource - result = self._client.invoke_capability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_args(self, args: typing.Iterable[str]) -> typing.Self: """Adds arguments""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9985,38 +9749,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentCallback', rpc_args)) else: raise TypeError("Invalid type for option 'env_callback'. Expected: Callable[[EnvironmentCallbackContext], None]") - if _env_expression := kwargs.pop("env_expression", None): - if _validate_tuple_types(_env_expression, (str, ReferenceExpression)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, ReferenceExpression], _env_expression)[0] - rpc_args["value"] = typing.cast(tuple[str, ReferenceExpression], _env_expression)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentExpression', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_expression'. Expected: (str, ReferenceExpression)") - if _env_endpoint := kwargs.pop("env_endpoint", None): - if _validate_tuple_types(_env_endpoint, (str, EndpointReference)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, EndpointReference], _env_endpoint)[0] - rpc_args["endpointReference"] = typing.cast(tuple[str, EndpointReference], _env_endpoint)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentEndpoint', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_endpoint'. Expected: (str, EndpointReference)") - if _env_parameter := kwargs.pop("env_parameter", None): - if _validate_tuple_types(_env_parameter, (str, ParameterResource)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["name"] = typing.cast(tuple[str, ParameterResource], _env_parameter)[0] - rpc_args["parameter"] = typing.cast(tuple[str, ParameterResource], _env_parameter)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentParameter', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_parameter'. Expected: (str, ParameterResource)") - if _env_connection_string := kwargs.pop("env_connection_string", None): - if _validate_tuple_types(_env_connection_string, (str, AbstractResourceWithConnectionString)): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["envVarName"] = typing.cast(tuple[str, AbstractResourceWithConnectionString], _env_connection_string)[0] - rpc_args["resource"] = typing.cast(tuple[str, AbstractResourceWithConnectionString], _env_connection_string)[1] - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEnvironmentConnectionString', rpc_args)) - else: - raise TypeError("Invalid type for option 'env_connection_string'. Expected: (str, AbstractResourceWithConnectionString)") if _args := kwargs.pop("args", None): if _validate_type(_args, typing.Iterable[str]): rpc_args: dict[str, typing.Any] = {"builder": handle} diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 7691950d6ec..f62c3b1d388 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1337,17 +1337,6 @@ impl CSharpAppResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -1359,39 +1348,6 @@ impl CSharpAppResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds arguments pub fn with_args(&self, args: Vec) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3546,17 +3502,6 @@ impl ContainerResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3568,39 +3513,6 @@ impl ContainerResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds arguments pub fn with_args(&self, args: Vec) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5268,17 +5180,6 @@ impl DotnetToolResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5290,39 +5191,6 @@ impl DotnetToolResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds arguments pub fn with_args(&self, args: Vec) -> Result> { let mut args: HashMap = HashMap::new(); @@ -6987,17 +6855,6 @@ impl ExecutableResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7009,39 +6866,6 @@ impl ExecutableResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds arguments pub fn with_args(&self, args: Vec) -> Result> { let mut args: HashMap = HashMap::new(); @@ -11552,17 +11376,6 @@ impl ProjectResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -11574,39 +11387,6 @@ impl ProjectResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds arguments pub fn with_args(&self, args: Vec) -> Result> { let mut args: HashMap = HashMap::new(); @@ -13522,17 +13302,6 @@ impl TestDatabaseResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -13544,39 +13313,6 @@ impl TestDatabaseResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds arguments pub fn with_args(&self, args: Vec) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14960,17 +14696,6 @@ impl TestRedisResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14982,39 +14707,6 @@ impl TestRedisResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds a connection property with a string or reference expression value pub fn with_connection_property(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16504,17 +16196,6 @@ impl TestVaultResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16526,39 +16207,6 @@ impl TestVaultResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a parameter resource - pub fn with_environment_parameter(&self, name: &str, parameter: &ParameterResource) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("parameter".to_string(), parameter.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentParameter", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Sets an environment variable from a connection string resource - pub fn with_environment_connection_string(&self, env_var_name: &str, resource: &IResourceWithConnectionString) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("envVarName".to_string(), serde_json::to_value(&env_var_name).unwrap_or(Value::Null)); - args.insert("resource".to_string(), resource.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentConnectionString", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Adds arguments pub fn with_args(&self, args: Vec) -> Result> { let mut args: HashMap = HashMap::new(); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs index 67bebfa7838..a057041c758 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs @@ -879,18 +879,6 @@ await Verify(aspireTs, extension: "ts") .UseFileName("TwoPassScanningGeneratedAspire"); } - [Fact] - public void TwoPassScanning_GeneratesDeprecatedJSDocForObsoleteExports() - { - var atsContext = CreateContextFromBothAssemblies(); - - var files = _generator.GenerateDistributedApplication(atsContext); - var aspireTs = files["aspire.ts"]; - - Assert.Contains("@deprecated ATS compatibility shim. Use withEnvironment instead.", aspireTs); - Assert.Contains("withEnvironmentExpression(name: string, value: ReferenceExpression)", aspireTs); - } - [Fact] public void TwoPassScanning_DeduplicatesExpandedUnionTypes() { diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt index 9cc9f1e1b07..84b6fd6c8bf 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt @@ -573,62 +573,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withEnvironmentConnectionString, - MethodName: withEnvironmentConnectionString, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, - { - CapabilityId: Aspire.Hosting/withEnvironmentEndpoint, - MethodName: withEnvironmentEndpoint, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, - { - CapabilityId: Aspire.Hosting/withEnvironmentExpression, - MethodName: withEnvironmentExpression, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, - { - CapabilityId: Aspire.Hosting/withEnvironmentParameter, - MethodName: withEnvironmentParameter, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withExplicitStart, MethodName: withExplicitStart, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 5a3f8804061..2a498bf3b06 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -8830,28 +8830,8 @@ export interface ContainerResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ContainerResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ContainerResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ContainerResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ContainerResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ContainerResourcePromise; /** Adds arguments */ withArgs(args: string[]): ContainerResourcePromise; /** Sets command-line arguments via callback */ @@ -9051,28 +9031,8 @@ export interface ContainerResourcePromise extends PromiseLike withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ContainerResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ContainerResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ContainerResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ContainerResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ContainerResourcePromise; /** Adds arguments */ withArgs(args: string[]): ContainerResourcePromise; /** Sets command-line arguments via callback */ @@ -9635,20 +9595,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEnvironmentInternal(name, value), this._client); } - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -9668,51 +9614,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEnvironmentCallbackInternal(callback), this._client); } - /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: Awaitable): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withArgsInternal(args: string[]): Promise { const rpcArgs: Record = { builder: this._handle, args }; @@ -11043,26 +10944,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withArgs(args: string[]): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withArgs(args)), this._client); } @@ -11379,28 +11264,8 @@ export interface CSharpAppResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): CSharpAppResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): CSharpAppResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): CSharpAppResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): CSharpAppResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): CSharpAppResourcePromise; /** Adds arguments */ withArgs(args: string[]): CSharpAppResourcePromise; /** Sets command-line arguments via callback */ @@ -11568,28 +11433,8 @@ export interface CSharpAppResourcePromise extends PromiseLike withRequiredCommand(command: string, options?: WithRequiredCommandOptions): CSharpAppResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): CSharpAppResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): CSharpAppResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): CSharpAppResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): CSharpAppResourcePromise; /** Adds arguments */ withArgs(args: string[]): CSharpAppResourcePromise; /** Sets command-line arguments via callback */ @@ -11912,20 +11757,6 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withEnvironmentInternal(name, value), this._client); } - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new CSharpAppResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -11945,51 +11776,6 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withEnvironmentCallbackInternal(callback), this._client); } - /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: Awaitable): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new CSharpAppResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new CSharpAppResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new CSharpAppResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withArgsInternal(args: string[]): Promise { const rpcArgs: Record = { builder: this._handle, args }; @@ -13253,26 +13039,10 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withArgs(args: string[]): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withArgs(args)), this._client); } @@ -13601,28 +13371,8 @@ export interface DotnetToolResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): DotnetToolResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): DotnetToolResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): DotnetToolResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): DotnetToolResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): DotnetToolResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): DotnetToolResourcePromise; /** Adds arguments */ withArgs(args: string[]): DotnetToolResourcePromise; /** Sets command-line arguments via callback */ @@ -13800,28 +13550,8 @@ export interface DotnetToolResourcePromise extends PromiseLike): DotnetToolResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): DotnetToolResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): DotnetToolResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): DotnetToolResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): DotnetToolResourcePromise; /** Adds arguments */ withArgs(args: string[]): DotnetToolResourcePromise; /** Sets command-line arguments via callback */ @@ -14224,20 +13954,6 @@ class DotnetToolResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new DotnetToolResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -14257,51 +13973,6 @@ class DotnetToolResourceImpl extends ResourceBuilderBase): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new DotnetToolResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new DotnetToolResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new DotnetToolResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withArgsInternal(args: string[]): Promise { const rpcArgs: Record = { builder: this._handle, args }; @@ -15574,26 +15245,10 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withArgs(args: string[]): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withArgs(args)), this._client); } @@ -15906,28 +15561,8 @@ export interface ExecutableResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ExecutableResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ExecutableResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ExecutableResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ExecutableResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ExecutableResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ExecutableResourcePromise; /** Adds arguments */ withArgs(args: string[]): ExecutableResourcePromise; /** Sets command-line arguments via callback */ @@ -16093,28 +15728,8 @@ export interface ExecutableResourcePromise extends PromiseLike): ExecutableResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ExecutableResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ExecutableResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ExecutableResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ExecutableResourcePromise; /** Adds arguments */ withArgs(args: string[]): ExecutableResourcePromise; /** Sets command-line arguments via callback */ @@ -16433,20 +16048,6 @@ class ExecutableResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ExecutableResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -16466,51 +16067,6 @@ class ExecutableResourceImpl extends ResourceBuilderBase): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new ExecutableResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new ExecutableResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new ExecutableResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withArgsInternal(args: string[]): Promise { const rpcArgs: Record = { builder: this._handle, args }; @@ -17759,26 +17315,10 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withArgs(args: string[]): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withArgs(args)), this._client); } @@ -20379,28 +19919,8 @@ export interface ProjectResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ProjectResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ProjectResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ProjectResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ProjectResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ProjectResourcePromise; /** Adds arguments */ withArgs(args: string[]): ProjectResourcePromise; /** Sets command-line arguments via callback */ @@ -20568,28 +20088,8 @@ export interface ProjectResourcePromise extends PromiseLike { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ProjectResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ProjectResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ProjectResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ProjectResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ProjectResourcePromise; /** Adds arguments */ withArgs(args: string[]): ProjectResourcePromise; /** Sets command-line arguments via callback */ @@ -20912,20 +20412,6 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withEnvironmentInternal(name, value), this._client); } - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ProjectResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -20945,51 +20431,6 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withEnvironmentCallbackInternal(callback), this._client); } - /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: Awaitable): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new ProjectResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new ProjectResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new ProjectResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withArgsInternal(args: string[]): Promise { const rpcArgs: Record = { builder: this._handle, args }; @@ -22253,26 +21694,10 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withArgs(args: string[]): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withArgs(args)), this._client); } @@ -22621,28 +22046,8 @@ export interface TestDatabaseResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestDatabaseResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestDatabaseResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestDatabaseResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestDatabaseResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): TestDatabaseResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestDatabaseResourcePromise; /** Adds arguments */ withArgs(args: string[]): TestDatabaseResourcePromise; /** Sets command-line arguments via callback */ @@ -22842,28 +22247,8 @@ export interface TestDatabaseResourcePromise extends PromiseLike): TestDatabaseResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestDatabaseResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestDatabaseResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): TestDatabaseResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestDatabaseResourcePromise; /** Adds arguments */ withArgs(args: string[]): TestDatabaseResourcePromise; /** Sets command-line arguments via callback */ @@ -23426,20 +22811,6 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -23459,51 +22830,6 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withArgsInternal(args: string[]): Promise { const rpcArgs: Record = { builder: this._handle, args }; @@ -24834,26 +24160,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withArgs(args: string[]): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withArgs(args)), this._client); } @@ -25202,28 +24512,8 @@ export interface TestRedisResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestRedisResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestRedisResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestRedisResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): TestRedisResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestRedisResourcePromise; /** Adds a connection property with a string or reference expression value */ withConnectionProperty(name: string, value: string | ReferenceExpression): TestRedisResourcePromise; /** Adds arguments */ @@ -25453,28 +24743,8 @@ export interface TestRedisResourcePromise extends PromiseLike withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestRedisResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestRedisResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestRedisResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): TestRedisResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestRedisResourcePromise; /** Adds a connection property with a string or reference expression value */ withConnectionProperty(name: string, value: string | ReferenceExpression): TestRedisResourcePromise; /** Adds arguments */ @@ -26067,20 +25337,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEnvironmentInternal(name, value), this._client); } - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -26100,51 +25356,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEnvironmentCallbackInternal(callback), this._client); } - /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: Awaitable): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withConnectionPropertyInternal(name: string, value: string | ReferenceExpression): Promise { const rpcArgs: Record = { builder: this._handle, name, value }; @@ -27673,26 +26884,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withConnectionProperty(name: string, value: string | ReferenceExpression): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withConnectionProperty(name, value)), this._client); } @@ -28101,28 +27296,8 @@ export interface TestVaultResource { withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestVaultResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestVaultResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestVaultResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): TestVaultResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestVaultResourcePromise; /** Adds arguments */ withArgs(args: string[]): TestVaultResourcePromise; /** Sets command-line arguments via callback */ @@ -28324,28 +27499,8 @@ export interface TestVaultResourcePromise extends PromiseLike withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestVaultResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestVaultResourcePromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestVaultResourcePromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): TestVaultResourcePromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestVaultResourcePromise; /** Adds arguments */ withArgs(args: string[]): TestVaultResourcePromise; /** Sets command-line arguments via callback */ @@ -28910,20 +28065,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEnvironmentInternal(name, value), this._client); } - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -28943,51 +28084,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEnvironmentCallbackInternal(callback), this._client); } - /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: Awaitable): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withArgsInternal(args: string[]): Promise { const rpcArgs: Record = { builder: this._handle, args }; @@ -30332,26 +29428,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withArgs(args: string[]): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withArgs(args)), this._client); } @@ -32737,28 +31817,8 @@ export interface ResourceWithEnvironment { withOtlpExporter(options?: WithOtlpExporterOptions): ResourceWithEnvironmentPromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ResourceWithEnvironmentPromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ResourceWithEnvironmentPromise; /** Configures which reference values are injected into environment variables */ withReferenceEnvironment(options: ReferenceEnvironmentInjectionOptions): ResourceWithEnvironmentPromise; /** Adds a reference to another resource */ @@ -32782,28 +31842,8 @@ export interface ResourceWithEnvironmentPromise extends PromiseLike): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from a reference expression - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ResourceWithEnvironmentPromise; /** Sets environment variables via callback */ withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from an endpoint reference - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from a parameter resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentParameter(name: string, parameter: Awaitable): ResourceWithEnvironmentPromise; - /** - * Sets an environment variable from a connection string resource - * @deprecated ATS compatibility shim. Use withEnvironment instead. - */ - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ResourceWithEnvironmentPromise; /** Configures which reference values are injected into environment variables */ withReferenceEnvironment(options: ReferenceEnvironmentInjectionOptions): ResourceWithEnvironmentPromise; /** Adds a reference to another resource */ @@ -32862,20 +31902,6 @@ class ResourceWithEnvironmentImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ResourceWithEnvironmentImpl(result, this._client); - } - - withEnvironmentExpression(name: string, value: ReferenceExpression): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._withEnvironmentExpressionInternal(name, value), this._client); - } - /** @internal */ private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -32895,51 +31921,6 @@ class ResourceWithEnvironmentImpl extends ResourceBuilderBase): Promise { - endpointReference = isPromiseLike(endpointReference) ? await endpointReference : endpointReference; - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', - rpcArgs - ); - return new ResourceWithEnvironmentImpl(result, this._client); - } - - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._withEnvironmentEndpointInternal(name, endpointReference), this._client); - } - - /** @internal */ - private async _withEnvironmentParameterInternal(name: string, parameter: Awaitable): Promise { - parameter = isPromiseLike(parameter) ? await parameter : parameter; - const rpcArgs: Record = { builder: this._handle, name, parameter }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentParameter', - rpcArgs - ); - return new ResourceWithEnvironmentImpl(result, this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._withEnvironmentParameterInternal(name, parameter), this._client); - } - - /** @internal */ - private async _withEnvironmentConnectionStringInternal(envVarName: string, resource: Awaitable): Promise { - resource = isPromiseLike(resource) ? await resource : resource; - const rpcArgs: Record = { builder: this._handle, envVarName, resource }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentConnectionString', - rpcArgs - ); - return new ResourceWithEnvironmentImpl(result, this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._withEnvironmentConnectionStringInternal(envVarName, resource), this._client); - } - /** @internal */ private async _withReferenceEnvironmentInternal(options: ReferenceEnvironmentInjectionOptions): Promise { const rpcArgs: Record = { builder: this._handle, options }; @@ -33094,26 +32075,10 @@ class ResourceWithEnvironmentPromiseImpl implements ResourceWithEnvironmentPromi return new ResourceWithEnvironmentPromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } - withEnvironmentExpression(name: string, value: ReferenceExpression): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._promise.then(obj => obj.withEnvironmentExpression(name, value)), this._client); - } - withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise { return new ResourceWithEnvironmentPromiseImpl(this._promise.then(obj => obj.withEnvironmentCallback(callback)), this._client); } - withEnvironmentEndpoint(name: string, endpointReference: Awaitable): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference)), this._client); - } - - withEnvironmentParameter(name: string, parameter: Awaitable): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter)), this._client); - } - - withEnvironmentConnectionString(envVarName: string, resource: Awaitable): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromiseImpl(this._promise.then(obj => obj.withEnvironmentConnectionString(envVarName, resource)), this._client); - } - withReferenceEnvironment(options: ReferenceEnvironmentInjectionOptions): ResourceWithEnvironmentPromise { return new ResourceWithEnvironmentPromiseImpl(this._promise.then(obj => obj.withReferenceEnvironment(options)), this._client); } From 835723ba7a1c38088b450adf52a88b2787402c27 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 11:57:57 -0700 Subject: [PATCH 16/55] [release/13.3] Add BrowserLogs tracked browser sessions (#16637) * Add BrowserLogs CDP transport seam Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use CDP pipe for BrowserLogs owned browsers Switch owned tracked-browser launches to a private CDP pipe, keep WebSocket adoption as an opt-in seam, and add session/persistent process lifetime configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove BrowserLogs persistent lifetime BrowserLogs pipe-launched browsers are always session scoped because Chromium exits when the CDP pipe closes. Remove the public lifetime option and mark public BrowserLogs types experimental. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Split BrowserLogs pipe launcher partials Move platform-specific native launch logic into Windows and Unix partial classes while keeping the shared launcher entry points in the common file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use posix_spawn for BrowserLogs Unix pipe launches Replace the managed fork/exec launcher with posix_spawn file actions so Chromium still receives CDP pipe fds 3 and 4 without running managed code in a forked child. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move BrowserLogs to Aspire.Hosting.Browsers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move BrowserLogs tests to Browsers assembly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update CI snapshots after BrowserLogs move Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update TypeScript capabilities snapshot Remove the BrowserLogs capability from the Hosting assembly scanner snapshot now that BrowserLogs lives in Aspire.Hosting.Browsers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove BrowserLogs friend assembly access Stop relying on InternalsVisibleTo for the Browsers package and its tests by source-sharing the BrowserLogs implementation into the test assembly and replacing Hosting-internal runtime dependencies with public-compatible behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore BrowserLogs health report publishing Add a public CustomResourceSnapshot helper for publishing health reports without InternalsVisibleTo and use it to restore BrowserLogs session and last-error health report rows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address BrowserLogs localization feedback Add translator comments for BrowserLogs resource strings with format placeholders and regenerate XLF files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix review findings in BrowserLogs - Fix null-forgiveness on screenshot Data property to throw InvalidOperationException instead of ArgumentNullException - Guard _stopCts.Cancel() against ObjectDisposedException when MonitorAsync cleanup races with StopAsync - Block '..' path traversal in SanitizePathSegment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: David Fowler Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Mitch Denny --- Aspire.slnx | 2 + .../BrowserTelemetry.AppHost.csproj | 1 + .../Aspire.Hosting.Browsers.csproj | 47 ++ .../BrowserConfiguration.cs | 23 +- .../BrowserConnectionDiagnosticsLogger.cs | 0 .../BrowserEndpointDiscovery.cs | 23 +- src/Aspire.Hosting.Browsers/BrowserHost.cs | 278 +++++++++++ .../BrowserHostRegistry.cs | 58 +-- .../BrowserLogsArtifacts.cs | 3 +- .../BrowserLogsBuilderExtensions.cs | 40 +- .../BrowserLogsCdpConnection.cs | 218 +++++--- .../BrowserLogsCdpConnectionMultiplexer.cs | 237 +++++++++ .../BrowserLogsCdpProtocol.cs | 0 .../BrowserLogsConfigurationManager.cs | 61 +-- .../BrowserLogsConfigurationStore.cs | 0 .../BrowserLogsEventLogger.cs | 0 .../BrowserLogsPipeBrowserProcess.cs | 65 +++ ...wserLogsPipeBrowserProcessLauncher.Unix.cs | 375 ++++++++++++++ ...rLogsPipeBrowserProcessLauncher.Windows.cs | 464 ++++++++++++++++++ .../BrowserLogsPipeBrowserProcessLauncher.cs | 31 ++ .../BrowserLogsProcessResult.cs | 9 + .../BrowserLogsResource.cs | 0 .../BrowserLogsRunningSession.cs | 15 +- .../BrowserLogsSessionManager.cs | 42 +- .../BrowserLogsUserDataDirectory.cs | 0 .../BrowserPageSession.cs | 20 +- .../BrowserUserDataMode.cs | 12 +- .../BrowserUserDataPathResolver.cs | 10 +- .../ChromiumBrowserResolver.cs | 14 +- .../ChromiumDevToolsActivePortParser.cs | 0 .../IBrowserHost.cs | 28 +- .../IBrowserLogsSessionManager.cs | 0 .../BrowserCommandStrings.Designer.cs | 52 ++ .../Resources/BrowserCommandStrings.resx | 119 +++++ .../BrowserMessageStrings.Designer.cs | 34 ++ .../Resources/BrowserMessageStrings.resx | 72 +++ .../xlf/BrowserCommandStrings.cs.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.de.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.es.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.fr.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.it.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.ja.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.ko.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.pl.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.pt-BR.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.ru.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.tr.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.zh-Hans.xlf | 172 +++++++ .../xlf/BrowserCommandStrings.zh-Hant.xlf | 172 +++++++ .../xlf/BrowserMessageStrings.cs.xlf | 82 ++++ .../xlf/BrowserMessageStrings.de.xlf | 82 ++++ .../xlf/BrowserMessageStrings.es.xlf | 82 ++++ .../xlf/BrowserMessageStrings.fr.xlf | 82 ++++ .../xlf/BrowserMessageStrings.it.xlf | 82 ++++ .../xlf/BrowserMessageStrings.ja.xlf | 82 ++++ .../xlf/BrowserMessageStrings.ko.xlf | 82 ++++ .../xlf/BrowserMessageStrings.pl.xlf | 82 ++++ .../xlf/BrowserMessageStrings.pt-BR.xlf | 82 ++++ .../xlf/BrowserMessageStrings.ru.xlf | 82 ++++ .../xlf/BrowserMessageStrings.tr.xlf | 82 ++++ .../xlf/BrowserMessageStrings.zh-Hans.xlf | 82 ++++ .../xlf/BrowserMessageStrings.zh-Hant.xlf | 82 ++++ .../CustomResourceSnapshotExtensions.cs | 31 ++ src/Aspire.Hosting/BrowserLogs/BrowserHost.cs | 420 ---------------- .../Resources/CommandStrings.Designer.cs | 297 ----------- .../Resources/CommandStrings.resx | 104 ---- .../Resources/MessageStrings.Designer.cs | 152 ------ .../Resources/MessageStrings.resx | 65 --- .../Resources/xlf/CommandStrings.cs.xlf | 165 ------- .../Resources/xlf/CommandStrings.de.xlf | 165 ------- .../Resources/xlf/CommandStrings.es.xlf | 165 ------- .../Resources/xlf/CommandStrings.fr.xlf | 165 ------- .../Resources/xlf/CommandStrings.it.xlf | 165 ------- .../Resources/xlf/CommandStrings.ja.xlf | 165 ------- .../Resources/xlf/CommandStrings.ko.xlf | 165 ------- .../Resources/xlf/CommandStrings.pl.xlf | 165 ------- .../Resources/xlf/CommandStrings.pt-BR.xlf | 165 ------- .../Resources/xlf/CommandStrings.ru.xlf | 165 ------- .../Resources/xlf/CommandStrings.tr.xlf | 165 ------- .../Resources/xlf/CommandStrings.zh-Hans.xlf | 165 ------- .../Resources/xlf/CommandStrings.zh-Hant.xlf | 165 ------- .../Resources/xlf/MessageStrings.cs.xlf | 85 ---- .../Resources/xlf/MessageStrings.de.xlf | 85 ---- .../Resources/xlf/MessageStrings.es.xlf | 85 ---- .../Resources/xlf/MessageStrings.fr.xlf | 85 ---- .../Resources/xlf/MessageStrings.it.xlf | 85 ---- .../Resources/xlf/MessageStrings.ja.xlf | 85 ---- .../Resources/xlf/MessageStrings.ko.xlf | 85 ---- .../Resources/xlf/MessageStrings.pl.xlf | 85 ---- .../Resources/xlf/MessageStrings.pt-BR.xlf | 85 ---- .../Resources/xlf/MessageStrings.ru.xlf | 85 ---- .../Resources/xlf/MessageStrings.tr.xlf | 85 ---- .../Resources/xlf/MessageStrings.zh-Hans.xlf | 85 ---- .../Resources/xlf/MessageStrings.zh-Hant.xlf | 85 ---- .../Aspire.Hosting.Browsers.Tests.csproj | 43 ++ ...BrowserConnectionDiagnosticsLoggerTests.cs | 2 +- .../BrowserHostTests.cs | 166 +++++++ .../BrowserLogsBuilderExtensionsTests.cs | 30 +- .../BrowserLogsCdpConnectionTests.cs | 274 ++++++++++- .../BrowserLogsCdpProtocolTests.cs | 2 +- ...wserLogsPipeBrowserProcessLauncherTests.cs | 93 ++++ .../BrowserLogsRunningSessionTests.cs | 10 +- .../BrowserLogsSessionManagerTests.cs | 15 +- .../BrowserPageSessionTests.cs | 24 +- .../ChromiumDevToolsActivePortParserTests.cs | 2 +- .../ConsoleLoggingTestHelpers.cs | 72 +++ ...TwoPassScanningGeneratedAspire.verified.go | 176 ------- ...oPassScanningGeneratedAspire.verified.java | 297 ----------- ...TwoPassScanningGeneratedAspire.verified.py | 99 ---- ...TwoPassScanningGeneratedAspire.verified.rs | 163 ------ ...ing.CodeGeneration.TypeScript.Tests.csproj | 1 + .../AtsTypeScriptCodeGeneratorTests.cs | 16 +- ...ContainerResourceCapabilities.verified.txt | 14 - ...TwoPassScanningGeneratedAspire.verified.ts | 264 ---------- .../ResourceNotificationTests.cs | 26 + 115 files changed, 6214 insertions(+), 5549 deletions(-) create mode 100644 src/Aspire.Hosting.Browsers/Aspire.Hosting.Browsers.csproj rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserConfiguration.cs (92%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserConnectionDiagnosticsLogger.cs (100%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserEndpointDiscovery.cs (94%) create mode 100644 src/Aspire.Hosting.Browsers/BrowserHost.cs rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserHostRegistry.cs (85%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsArtifacts.cs (97%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsBuilderExtensions.cs (91%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsCdpConnection.cs (76%) create mode 100644 src/Aspire.Hosting.Browsers/BrowserLogsCdpConnectionMultiplexer.cs rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsCdpProtocol.cs (100%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsConfigurationManager.cs (86%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsConfigurationStore.cs (100%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsEventLogger.cs (100%) create mode 100644 src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcess.cs create mode 100644 src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Unix.cs create mode 100644 src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Windows.cs create mode 100644 src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.cs create mode 100644 src/Aspire.Hosting.Browsers/BrowserLogsProcessResult.cs rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsResource.cs (100%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsRunningSession.cs (95%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsSessionManager.cs (94%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserLogsUserDataDirectory.cs (100%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserPageSession.cs (95%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserUserDataMode.cs (72%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/BrowserUserDataPathResolver.cs (94%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/ChromiumBrowserResolver.cs (94%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/ChromiumDevToolsActivePortParser.cs (100%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/IBrowserHost.cs (86%) rename src/{Aspire.Hosting/BrowserLogs => Aspire.Hosting.Browsers}/IBrowserLogsSessionManager.cs (100%) create mode 100644 src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.Designer.cs create mode 100644 src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.resx create mode 100644 src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.Designer.cs create mode 100644 src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.resx create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.cs.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.de.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.es.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.fr.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.it.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ja.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ko.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pl.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pt-BR.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ru.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.tr.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hans.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hant.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.cs.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.de.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.es.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.fr.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.it.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ja.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ko.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pl.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pt-BR.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ru.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.tr.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hans.xlf create mode 100644 src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hant.xlf create mode 100644 src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshotExtensions.cs delete mode 100644 src/Aspire.Hosting/BrowserLogs/BrowserHost.cs create mode 100644 tests/Aspire.Hosting.Browsers.Tests/Aspire.Hosting.Browsers.Tests.csproj rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/BrowserConnectionDiagnosticsLoggerTests.cs (98%) create mode 100644 tests/Aspire.Hosting.Browsers.Tests/BrowserHostTests.cs rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/BrowserLogsBuilderExtensionsTests.cs (98%) rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/BrowserLogsCdpConnectionTests.cs (58%) rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/BrowserLogsCdpProtocolTests.cs (99%) create mode 100644 tests/Aspire.Hosting.Browsers.Tests/BrowserLogsPipeBrowserProcessLauncherTests.cs rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/BrowserLogsRunningSessionTests.cs (94%) rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/BrowserLogsSessionManagerTests.cs (97%) rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/BrowserPageSessionTests.cs (95%) rename tests/{Aspire.Hosting.Tests => Aspire.Hosting.Browsers.Tests}/ChromiumDevToolsActivePortParserTests.cs (96%) create mode 100644 tests/Aspire.Hosting.Browsers.Tests/ConsoleLoggingTestHelpers.cs diff --git a/Aspire.slnx b/Aspire.slnx index 176117e6146..18fa49a110f 100644 --- a/Aspire.slnx +++ b/Aspire.slnx @@ -50,6 +50,7 @@ + @@ -477,6 +478,7 @@ + diff --git a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/BrowserTelemetry.AppHost.csproj b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/BrowserTelemetry.AppHost.csproj index 24805d703bf..99cff6873ed 100644 --- a/playground/BrowserTelemetry/BrowserTelemetry.AppHost/BrowserTelemetry.AppHost.csproj +++ b/playground/BrowserTelemetry/BrowserTelemetry.AppHost/BrowserTelemetry.AppHost.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Aspire.Hosting.Browsers/Aspire.Hosting.Browsers.csproj b/src/Aspire.Hosting.Browsers/Aspire.Hosting.Browsers.csproj new file mode 100644 index 00000000000..fceb94fb8fc --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Aspire.Hosting.Browsers.csproj @@ -0,0 +1,47 @@ + + + + $(DefaultTargetFramework) + true + aspire integration hosting browser browsers logs diagnostics + Browser support for Aspire hosting. + + true + true + + + + + + + + + + + + + + + True + True + BrowserCommandStrings.resx + + + True + True + BrowserMessageStrings.resx + + + + + + ResXFileCodeGenerator + BrowserCommandStrings.Designer.cs + + + ResXFileCodeGenerator + BrowserMessageStrings.Designer.cs + + + + diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserConfiguration.cs b/src/Aspire.Hosting.Browsers/BrowserConfiguration.cs similarity index 92% rename from src/Aspire.Hosting/BrowserLogs/BrowserConfiguration.cs rename to src/Aspire.Hosting.Browsers/BrowserConfiguration.cs index 09e7ebae147..47e03c15f69 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserConfiguration.cs +++ b/src/Aspire.Hosting.Browsers/BrowserConfiguration.cs @@ -1,8 +1,10 @@ // 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 ASPIREBROWSERLOGS001 // Type is for evaluation purposes only + using System.Globalization; -using Aspire.Hosting.Resources; +using Aspire.Hosting.Browsers.Resources; using Microsoft.Extensions.Configuration; namespace Aspire.Hosting; @@ -15,7 +17,11 @@ namespace Aspire.Hosting; /// does that imply?". The later user-data-directory decision belongs to , where /// the resolved browser executable path is available. /// -internal readonly record struct BrowserConfiguration(string Browser, string? Profile, BrowserUserDataMode UserDataMode, string? AppHostKey) +internal readonly record struct BrowserConfiguration( + string Browser, + string? Profile, + BrowserUserDataMode UserDataMode, + string? AppHostKey) { /// /// The default mode points at an Aspire-managed persistent user data directory shared across every Aspire @@ -48,12 +54,12 @@ internal static BrowserConfiguration Resolve( if (string.IsNullOrWhiteSpace(resolvedBrowser)) { - throw new InvalidOperationException(MessageStrings.BrowserLogsEmptyBrowserConfiguration); + throw new InvalidOperationException(BrowserMessageStrings.BrowserLogsEmptyBrowserConfiguration); } if (resolvedProfile is not null && string.IsNullOrWhiteSpace(resolvedProfile)) { - throw new InvalidOperationException(MessageStrings.BrowserLogsEmptyProfileConfiguration); + throw new InvalidOperationException(BrowserMessageStrings.BrowserLogsEmptyProfileConfiguration); } if (resolvedUserDataMode == BrowserUserDataMode.Isolated && resolvedProfile is not null) @@ -61,7 +67,7 @@ internal static BrowserConfiguration Resolve( throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - MessageStrings.BrowserLogsProfileRequiresSharedUserDataMode, + BrowserMessageStrings.BrowserLogsProfileRequiresSharedUserDataMode, BrowserLogsBuilderExtensions.ProfileConfigurationKey, resolvedProfile, BrowserLogsBuilderExtensions.UserDataModeConfigurationKey, @@ -122,7 +128,7 @@ private static ConfigurationValue ParseUserDataMode(string? throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - MessageStrings.BrowserLogsInvalidUserDataModeConfiguration, + BrowserMessageStrings.BrowserLogsInvalidUserDataModeConfiguration, value, BrowserLogsBuilderExtensions.UserDataModeConfigurationKey, BrowserUserDataMode.Shared, @@ -238,4 +244,7 @@ private readonly record struct ConfigurationValue(bool HasValue, T Value) /// /// Browser configuration values explicitly supplied by the resource builder. /// -internal readonly record struct BrowserConfigurationExplicitValues(string? Browser, string? Profile, BrowserUserDataMode? UserDataMode); +internal readonly record struct BrowserConfigurationExplicitValues( + string? Browser = null, + string? Profile = null, + BrowserUserDataMode? UserDataMode = null); diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserConnectionDiagnosticsLogger.cs b/src/Aspire.Hosting.Browsers/BrowserConnectionDiagnosticsLogger.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/BrowserConnectionDiagnosticsLogger.cs rename to src/Aspire.Hosting.Browsers/BrowserConnectionDiagnosticsLogger.cs diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserEndpointDiscovery.cs b/src/Aspire.Hosting.Browsers/BrowserEndpointDiscovery.cs similarity index 94% rename from src/Aspire.Hosting/BrowserLogs/BrowserEndpointDiscovery.cs rename to src/Aspire.Hosting.Browsers/BrowserEndpointDiscovery.cs index 5add159a5bf..9598f358b98 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserEndpointDiscovery.cs +++ b/src/Aspire.Hosting.Browsers/BrowserEndpointDiscovery.cs @@ -2,23 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Aspire.Hosting.Browsers.Resources; using System.Globalization; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; -using Aspire.Hosting.Resources; using Microsoft.Extensions.Logging; namespace Aspire.Hosting; -// Bridges owned and adopted hosts by persisting and validating the browser-level CDP endpoint for a shared -// user-data directory. +// Supports WebSocket attach/adoption by persisting and validating the browser-level CDP endpoint for a shared user-data +// directory. // // Why this exists: -// - Chromium's singleton is keyed by the user data root, not by Aspire. If Aspire already launched a debug-enabled -// browser for that root, a later browser-log session should attach to it instead of starting another process. +// - Chromium's singleton is keyed by the user data root, not by Aspire. If a WebSocket-based option already launched a +// debug-enabled browser for that root, a later browser-log session can attach to it instead of starting another +// process. // - Chromium's DevToolsActivePort file is only a launch-time hand-off file and isn't enough for cross-session adoption. -// We write our own sidecar with the exact browser identity and endpoint we proved during startup. +// This sidecar records the exact browser identity and endpoint proved during startup. // - The sidecar is intentionally treated as a hint. Users can close the browser, edit/delete files, or reuse ports, so // every read revalidates schema, identity, PID liveness, endpoint reachability, and profile compatibility. internal sealed class BrowserEndpointDiscovery(ILogger logger) @@ -138,10 +139,10 @@ metadataUserDataRootPath is null || throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - MessageStrings.BrowserLogsTrackedBrowserProfileConflict, + BrowserMessageStrings.BrowserLogsTrackedBrowserProfileConflict, identity.UserDataRootPath, - metadata.ProfileDirectoryName ?? MessageStrings.BrowserLogsDefaultProfileName, - profileDirectoryName ?? MessageStrings.BrowserLogsDefaultProfileName)); + metadata.ProfileDirectoryName ?? BrowserMessageStrings.BrowserLogsDefaultProfileName, + profileDirectoryName ?? BrowserMessageStrings.BrowserLogsDefaultProfileName)); } return metadata with { Endpoint = endpoint.ToString() }; @@ -288,7 +289,7 @@ private static void TryDelete(string path) } } -// On-disk adoption hint written by an owned host. A matching file never proves adoption is safe by itself; it must be +// On-disk adoption hint for WebSocket-backed hosts. A matching file never proves adoption is safe by itself; it must be // validated against the requested identity, profile, process, and /json/version endpoint first. internal sealed record BrowserDebugEndpointMetadata { @@ -319,7 +320,7 @@ internal sealed record BrowserJsonVersionResponse public string? WebSocketDebuggerUrl { get; init; } } -// Source-generated JSON context for the small metadata file exchanged between owned and adopted host paths and the +// Source-generated JSON context for the small metadata file exchanged between WebSocket attach/adoption paths and the // Chromium /json/version probe response. [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(BrowserDebugEndpointMetadata))] diff --git a/src/Aspire.Hosting.Browsers/BrowserHost.cs b/src/Aspire.Hosting.Browsers/BrowserHost.cs new file mode 100644 index 00000000000..d45d9f785e8 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/BrowserHost.cs @@ -0,0 +1,278 @@ +// 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only + +using Microsoft.Extensions.Logging; + +namespace Aspire.Hosting; + +// Base implementation for browser hosts. It centralizes the shared mechanics for creating per-page sessions +// while concrete hosts decide who owns the browser process lifetime. +internal abstract class BrowserHost( + BrowserHostIdentity identity, + BrowserHostOwnership ownership, + Uri? debugEndpoint, + string browserDisplayName, + ILogger logger, + TimeProvider timeProvider, + bool reuseInitialBlankTarget) : IBrowserHost +{ + private readonly ILogger _logger = logger; + private readonly bool _reuseInitialBlankTarget = reuseInitialBlankTarget; + private readonly TimeProvider _timeProvider = timeProvider; + + public BrowserHostIdentity Identity { get; } = identity; + + public BrowserHostOwnership Ownership { get; } = ownership; + + public Uri? DebugEndpoint { get; } = debugEndpoint; + + public abstract int? ProcessId { get; } + + public string BrowserDisplayName { get; } = browserDisplayName; + + public abstract Task Termination { get; } + + public virtual async Task CreateCdpConnectionAsync( + Func eventHandler, + ILogger logger, + CancellationToken cancellationToken) + { + var debugEndpoint = DebugEndpoint ?? throw new InvalidOperationException("Tracked browser host does not expose a WebSocket debug endpoint."); + return await BrowserLogsCdpConnection.ConnectAsync(debugEndpoint, eventHandler, logger, cancellationToken).ConfigureAwait(false); + } + + public Task CreatePageSessionAsync( + string sessionId, + Uri url, + BrowserConnectionDiagnosticsLogger connectionDiagnostics, + Func eventHandler, + CancellationToken cancellationToken) + { + return CreatePageSessionCoreAsync(sessionId, url, connectionDiagnostics, eventHandler, cancellationToken); + } + + public abstract ValueTask DisposeAsync(); + + private async Task CreatePageSessionCoreAsync( + string sessionId, + Uri url, + BrowserConnectionDiagnosticsLogger connectionDiagnostics, + Func eventHandler, + CancellationToken cancellationToken) + { + return await BrowserPageSession.StartAsync( + this, + sessionId, + url, + connectionDiagnostics, + eventHandler, + _logger, + _timeProvider, + _reuseInitialBlankTarget, + cancellationToken).ConfigureAwait(false); + } +} + +// Host implementation for browsers Aspire starts itself. Owned hosts are responsible for spawning Chromium with a +// private browser-level CDP pipe and cleaning it up when the final lease is released. +internal sealed class OwnedBrowserHost : BrowserHost +{ + private readonly BrowserLogsCdpConnectionMultiplexer _connectionMultiplexer; + private readonly IBrowserLogsPipeBrowserProcess _process; + private readonly BrowserLogsUserDataDirectory _userDataDirectory; + private readonly Task _processTask; + private readonly Task _termination; + private int _disposed; + + private OwnedBrowserHost( + BrowserHostIdentity identity, + string browserDisplayName, + IBrowserLogsPipeBrowserProcess process, + BrowserLogsCdpConnectionMultiplexer connectionMultiplexer, + BrowserLogsUserDataDirectory userDataDirectory, + ILogger logger, + TimeProvider timeProvider) + : base(identity, BrowserHostOwnership.Owned, debugEndpoint: null, browserDisplayName, logger, timeProvider, reuseInitialBlankTarget: true) + { + _connectionMultiplexer = connectionMultiplexer; + _process = process; + _processTask = process.ProcessTask; + _termination = CompleteWhenProcessOrPipeEndsAsync(process.ProcessTask, connectionMultiplexer.Completion); + _userDataDirectory = userDataDirectory; + ProcessId = process.ProcessId; + } + + public override int? ProcessId { get; } + + public override Task Termination => _termination; + + public override Task CreateCdpConnectionAsync( + Func eventHandler, + ILogger logger, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(_connectionMultiplexer.CreateConnection(eventHandler)); + } + + private static List BuildBrowserArguments(BrowserLogsUserDataDirectory userDataDirectory) + { + // The initial about:blank page gives owned hosts a predictable first page target that can be navigated instead + // of leaving an extra blank tab. + List arguments = + [ + $"--user-data-dir={userDataDirectory.Path}", + "--no-first-run", + "--no-default-browser-check", + "--new-window", + "--allow-insecure-localhost" + ]; + + if (userDataDirectory.ProfileDirectoryName is { } profileDirectoryName) + { + arguments.Add($"--profile-directory={profileDirectoryName}"); + } + + arguments.Add("about:blank"); + return arguments; + } + + public static async Task StartAsync( + BrowserHostIdentity identity, + string browserDisplayName, + BrowserLogsUserDataDirectory userDataDirectory, + ILogger logger, + TimeProvider timeProvider, + CancellationToken cancellationToken, + Func, IBrowserLogsPipeBrowserProcess>? startPipeBrowserProcess = null) + { + var devToolsActivePortFilePath = Path.Combine(userDataDirectory.Path, "DevToolsActivePort"); + // Pipe-backed launches do not use DevToolsActivePort or the sidecar endpoint file. Clear stale WebSocket + // hand-off metadata before creating the private-pipe browser so future attach/adoption code doesn't mistake it + // for current state. + DeleteBrowserEndpointFile(devToolsActivePortFilePath, logger); + BrowserEndpointDiscovery.DeleteEndpointMetadata(userDataDirectory.Path); + startPipeBrowserProcess ??= BrowserLogsPipeBrowserProcessLauncher.Start; + + IBrowserLogsPipeBrowserProcess? process = null; + BrowserLogsCdpConnectionMultiplexer? connectionMultiplexer = null; + try + { + cancellationToken.ThrowIfCancellationRequested(); + process = startPipeBrowserProcess(identity.ExecutablePath, BuildBrowserArguments(userDataDirectory)); + connectionMultiplexer = new BrowserLogsCdpConnectionMultiplexer( + new BrowserLogsPipeCdpTransport(process.BrowserOutput, process.BrowserInput), + logger); + } + catch + { + if (connectionMultiplexer is not null) + { + await connectionMultiplexer.DisposeAsync().ConfigureAwait(false); + } + + if (process is not null) + { + await process.DisposeAsync().ConfigureAwait(false); + } + + userDataDirectory.Dispose(); + throw; + } + + return new OwnedBrowserHost( + identity, + browserDisplayName, + process, + connectionMultiplexer, + userDataDirectory, + logger, + timeProvider); + } + + public override async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + try + { + await _connectionMultiplexer.DisposeAsync().ConfigureAwait(false); + } + finally + { + try + { + await _process.DisposeAsync().ConfigureAwait(false); + } + finally + { + _ = _processTask; + _userDataDirectory.Dispose(); + } + } + } + + private static async Task CompleteWhenProcessOrPipeEndsAsync(Task processTask, Task pipeTask) + { + // Pipe-backed hosts cannot reconnect: the only CDP pipe is owned by this AppHost process. Treat either browser + // process exit or pipe failure as host termination so page sessions end instead of running WebSocket-style + // reconnect loops against a dead private transport. + _ = await Task.WhenAny(processTask, pipeTask).ConfigureAwait(false); + } + + private static void DeleteBrowserEndpointFile(string devToolsActivePortFilePath, ILogger logger) + { + if (!File.Exists(devToolsActivePortFilePath)) + { + return; + } + + try + { + File.Delete(devToolsActivePortFilePath); + } + catch (IOException ex) + { + logger.LogDebug(ex, "Unable to delete stale tracked browser endpoint metadata '{DevToolsActivePortFilePath}'.", devToolsActivePortFilePath); + } + catch (UnauthorizedAccessException ex) + { + logger.LogDebug(ex, "Unable to delete stale tracked browser endpoint metadata '{DevToolsActivePortFilePath}'.", devToolsActivePortFilePath); + } + } +} + +// Host implementation for browsers Aspire discovers from validated endpoint metadata. Adopted hosts use WebSocket CDP +// and create/close tracked targets, but never terminate the browser process because it may outlive this AppHost. +internal sealed class AdoptedBrowserHost : BrowserHost +{ + private readonly TaskCompletionSource _terminationSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + // An adopted browser may already contain user-owned tabs. Always create a new target for Aspire rather than reusing + // an arbitrary about:blank page that happened to exist in the browser. + public AdoptedBrowserHost( + BrowserHostIdentity identity, + Uri debugEndpoint, + string browserDisplayName, + ILogger logger, + TimeProvider timeProvider) + : base(identity, BrowserHostOwnership.Adopted, debugEndpoint, browserDisplayName, logger, timeProvider, reuseInitialBlankTarget: false) + { + } + + public override int? ProcessId => null; + + public override Task Termination => _terminationSource.Task; + + public override ValueTask DisposeAsync() + { + _terminationSource.TrySetResult(); + + return ValueTask.CompletedTask; + } +} diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserHostRegistry.cs b/src/Aspire.Hosting.Browsers/BrowserHostRegistry.cs similarity index 85% rename from src/Aspire.Hosting/BrowserLogs/BrowserHostRegistry.cs rename to src/Aspire.Hosting.Browsers/BrowserHostRegistry.cs index 4002b23289e..de80a3a982d 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserHostRegistry.cs +++ b/src/Aspire.Hosting.Browsers/BrowserHostRegistry.cs @@ -4,8 +4,8 @@ #pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only using System.Diagnostics; +using Aspire.Hosting.Browsers.Resources; using System.Globalization; -using Aspire.Hosting.Resources; using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -19,6 +19,7 @@ internal sealed class BrowserHostRegistry : IAsyncDisposable private readonly Func _createUserDataDirectory; private readonly Func> _createHostAsync; private readonly Dictionary _hosts = new(); + private readonly bool _enableEndpointMetadataAdoption; private readonly SemaphoreSlim _lock = new(1, 1); private readonly object _lockLifetimeGate = new(); private readonly ILogger _logger; @@ -37,11 +38,13 @@ internal BrowserHostRegistry( ILogger logger, TimeProvider timeProvider, Func? createUserDataDirectory, - Func>? createHostAsync) + Func>? createHostAsync, + bool enableEndpointMetadataAdoption = false) { _endpointDiscovery = new BrowserEndpointDiscovery(logger); _createUserDataDirectory = createUserDataDirectory ?? CreateUserDataDirectory; _createHostAsync = createHostAsync ?? CreateHostCoreAsync; + _enableEndpointMetadataAdoption = enableEndpointMetadataAdoption; _logger = logger; _timeProvider = timeProvider; } @@ -51,7 +54,7 @@ public async Task AcquireAsync(BrowserConfiguration configurat ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); var browserExecutable = ChromiumBrowserResolver.TryResolveExecutable(configuration.Browser) - ?? throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsUnableToLocateBrowser, configuration.Browser)); + ?? throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsUnableToLocateBrowser, configuration.Browser)); var userDataDirectory = _createUserDataDirectory(configuration, browserExecutable); var identity = new BrowserHostIdentity(browserExecutable, userDataDirectory.Path); @@ -61,10 +64,9 @@ public async Task AcquireAsync(BrowserConfiguration configurat // count. // 2. Otherwise, create a host exactly once and publish it into the registry with the first lease. // - // Keep the lock held across CreateHostCoreAsync. That method may adopt an existing debug-enabled browser or - // start a new process, both of which depend on filesystem endpoint metadata for the same user data root. If two - // callers ran that decision concurrently they could both miss the dictionary entry and race to adopt/start a - // browser for the same profile. + // Keep the lock held across CreateHostCoreAsync. That method starts a new process by default, and can adopt a + // WebSocket endpoint when an explicit attach mode enables endpoint metadata. If two callers ran that decision + // concurrently they could both miss the dictionary entry and race to adopt/start a browser for the same profile. var lockAcquired = false; var hostPublished = false; try @@ -82,16 +84,16 @@ public async Task AcquireAsync(BrowserConfiguration configurat // as more resources start browser-log sessions, rather than one browser process per session. ValidateProfileCompatibility(identity, entry.ProfileDirectoryName, userDataDirectory.ProfileDirectoryName); entry.ReferenceCount++; - _logger.LogInformation("Reusing tracked browser host '{BrowserExecutable}' at '{Endpoint}'. Active leases: {ReferenceCount}.", identity.ExecutablePath, entry.Host.DebugEndpoint, entry.ReferenceCount); + _logger.LogInformation("Reusing tracked browser host '{BrowserExecutable}' at '{Endpoint}'. Active leases: {ReferenceCount}.", identity.ExecutablePath, FormatDebugEndpoint(entry.Host.DebugEndpoint), entry.ReferenceCount); userDataDirectory.Dispose(); return new BrowserHostLease(entry.Host, releaseAsync: token => ReleaseAsync(identity, token)); } - // No host exists for this identity yet. CreateHostCoreAsync owns the second-stage decision: - // adopt a validated shared browser if one is already running, reject an incompatible locked profile, or - // start a new owned browser. The returned host is inserted before returning the first lease so future - // callers can reuse it. This keeps the visible behavior stable when several resources request browser logs - // together: the first request may open/adopt the browser, and the rest should attach to that result. + // No host exists for this identity yet. CreateHostCoreAsync owns the second-stage decision: start a new + // pipe-owned browser by default, or adopt a validated WebSocket endpoint if an explicit attach mode enabled + // that path. The returned host is inserted before returning the first lease so future callers can reuse it. + // This keeps the visible behavior stable when several resources request browser logs together: the first + // request opens/adopts the browser, and the rest attach to that result. var host = await _createHostAsync(configuration, identity, userDataDirectory, cancellationToken).ConfigureAwait(false); _hosts[identity] = new BrowserHostEntry(host, userDataDirectory.ProfileDirectoryName, ReferenceCount: 1); hostPublished = true; @@ -293,17 +295,11 @@ private async Task CreateHostCoreAsync( BrowserLogsUserDataDirectory userDataDirectory, CancellationToken cancellationToken) { - // Both Shared and Isolated point at an Aspire-managed persistent user data directory, so the same - // adoption-or-launch decision applies to both: - // - // 1. If a previous AppHost run for this identity wrote an adoption sidecar and the recorded process is - // still alive with a live /json/version endpoint, connect to that browser via CDP. - // 2. Otherwise, launch a new debug-enabled browser against the same user data directory and write the - // sidecar so the next AppHost run can connect. - // - // Aspire never points at the user's real browser profile, so a "non-debuggable browser is using this - // user data dir" failure mode no longer applies. - if (await _endpointDiscovery.TryReadAndValidateAsync(identity, userDataDirectory.ProfileDirectoryName, cancellationToken).ConfigureAwait(false) is { } metadata) + // Default owned launches use a process-private CDP pipe. WebSocket remains the attach/adoption transport for + // explicit connect-to-existing-browser modes, but the normal path must not adopt stale endpoint metadata from + // earlier WebSocket experiments because pipe-backed browsers cannot be reattached across AppHost processes. + if (_enableEndpointMetadataAdoption && + await _endpointDiscovery.TryReadAndValidateAsync(identity, userDataDirectory.ProfileDirectoryName, cancellationToken).ConfigureAwait(false) is { } metadata) { var endpoint = new Uri(metadata.Endpoint, UriKind.Absolute); _logger.LogInformation("Adopting tracked browser host '{BrowserExecutable}' at '{Endpoint}'.", identity.ExecutablePath, endpoint); @@ -311,7 +307,7 @@ private async Task CreateHostCoreAsync( return new AdoptedBrowserHost(identity, endpoint, configuration.Browser, _logger, _timeProvider); } - _logger.LogInformation("Starting tracked browser host '{BrowserExecutable}'.", identity.ExecutablePath); + _logger.LogInformation("Starting tracked browser host '{BrowserExecutable}' with a private CDP pipe.", identity.ExecutablePath); return await OwnedBrowserHost.StartAsync(identity, configuration.Browser, userDataDirectory, _logger, _timeProvider, cancellationToken).ConfigureAwait(false); } @@ -321,8 +317,9 @@ private BrowserLogsUserDataDirectory CreateUserDataDirectory(BrowserConfiguratio // Shared -> machine-wide, shared across every Aspire AppHost // Isolated -> per-AppHost (keyed on AppHost:PathSha256) // - // The directory is created on demand and never deleted by AppHost shutdown. The browser process is left - // running across AppHost runs and adopted via the endpoint sidecar. + // The directory is created on demand and not deleted by AppHost shutdown. The browser process itself is + // pipe-backed and defaults to Session lifetime, so each new AppHost run starts its own debuggable browser + // process unless an advanced lifetime option intentionally leaves the old browser running. var path = BrowserUserDataPathResolver.Resolve(configuration); // Profile resolution requires Local State to exist (Chromium writes it on first launch). Skip resolution @@ -368,12 +365,15 @@ private static void ValidateProfileCompatibility(BrowserHostIdentity identity, s throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - MessageStrings.BrowserLogsTrackedBrowserProfileConflict, + BrowserMessageStrings.BrowserLogsTrackedBrowserProfileConflict, identity.UserDataRootPath, - existingProfileDirectoryName ?? MessageStrings.BrowserLogsDefaultProfileName, + existingProfileDirectoryName ?? BrowserMessageStrings.BrowserLogsDefaultProfileName, requestedProfileDirectoryName)); } + private static string FormatDebugEndpoint(Uri? debugEndpoint) => + debugEndpoint?.ToString() ?? "private CDP pipe"; + private sealed class BrowserHostEntry(IBrowserHost host, string? profileDirectoryName, int ReferenceCount) { public IBrowserHost Host { get; } = host; diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsArtifacts.cs b/src/Aspire.Hosting.Browsers/BrowserLogsArtifacts.cs similarity index 97% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsArtifacts.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsArtifacts.cs index 61ba1987391..f9f99fbdf76 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserLogsArtifacts.cs +++ b/src/Aspire.Hosting.Browsers/BrowserLogsArtifacts.cs @@ -154,7 +154,8 @@ private static string SanitizePathSegment(string value, string fallback) } } - return hasValidCharacter ? new string(buffer) : fallback; + var sanitized = hasValidCharacter ? new string(buffer) : fallback; + return sanitized is "." or ".." ? fallback : sanitized; } } diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsBuilderExtensions.cs b/src/Aspire.Hosting.Browsers/BrowserLogsBuilderExtensions.cs similarity index 91% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsBuilderExtensions.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsBuilderExtensions.cs index 23c0f45074a..a92187b5e3b 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserLogsBuilderExtensions.cs +++ b/src/Aspire.Hosting.Browsers/BrowserLogsBuilderExtensions.cs @@ -2,13 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only +#pragma warning disable ASPIREBROWSERLOGS001 // Type is for evaluation purposes only using System.Collections.Immutable; +using Aspire.Hosting.Browsers.Resources; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Resources; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -18,6 +19,7 @@ namespace Aspire.Hosting; /// /// Extension methods for adding tracked browser log resources to browser-based application resources. /// +[Experimental("ASPIREBROWSERLOGS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static class BrowserLogsBuilderExtensions { internal const string BrowserResourceType = "BrowserLogs"; @@ -90,13 +92,12 @@ public static class BrowserLogsBuilderExtensions /// endpoints when selecting the browser target URL. /// /// - /// Browser, profile, and user data mode settings can also be supplied from configuration using - /// Aspire:Hosting:BrowserLogs:Browser, Aspire:Hosting:BrowserLogs:Profile, and - /// Aspire:Hosting:BrowserLogs:UserDataMode, or scoped to a specific resource with + /// Browser, profile, and user data mode settings can also be supplied from configuration + /// using Aspire:Hosting:BrowserLogs:Browser, Aspire:Hosting:BrowserLogs:Profile, + /// and Aspire:Hosting:BrowserLogs:UserDataMode, or scoped to a specific resource with /// Aspire:Hosting:BrowserLogs:{ResourceName}:Browser, /// Aspire:Hosting:BrowserLogs:{ResourceName}:Profile, and - /// Aspire:Hosting:BrowserLogs:{ResourceName}:UserDataMode. Explicit method arguments override - /// configuration. + /// Aspire:Hosting:BrowserLogs:{ResourceName}:UserDataMode. Explicit method arguments override configuration. /// /// /// @@ -149,7 +150,7 @@ public static IResourceBuilder WithBrowserLogs( }) .WithCommand( OpenTrackedBrowserCommandName, - CommandStrings.OpenTrackedBrowserName, + BrowserCommandStrings.OpenTrackedBrowserName, async context => { try @@ -169,7 +170,7 @@ public static IResourceBuilder WithBrowserLogs( }, new CommandOptions { - Description = CommandStrings.OpenTrackedBrowserDescription, + Description = BrowserCommandStrings.OpenTrackedBrowserDescription, IconName = "Open", IconVariant = IconVariant.Regular, IsHighlighted = true, @@ -182,15 +183,12 @@ public static IResourceBuilder WithBrowserLogs( } var resourceNotifications = context.ServiceProvider.GetRequiredService(); - foreach (var resourceName in parentResource.GetResolvedResourceNames()) + if (resourceNotifications.TryGetCurrentState(parentResource.Name, out var resourceEvent)) { - if (resourceNotifications.TryGetCurrentState(resourceName, out var resourceEvent)) + var parentState = resourceEvent.Snapshot.State?.Text; + if (parentState == KnownResourceStates.Running || parentState == KnownResourceStates.RuntimeUnhealthy) { - var parentState = resourceEvent.Snapshot.State?.Text; - if (parentState == KnownResourceStates.Running || parentState == KnownResourceStates.RuntimeUnhealthy) - { - return ResourceCommandState.Enabled; - } + return ResourceCommandState.Enabled; } } @@ -199,7 +197,7 @@ public static IResourceBuilder WithBrowserLogs( }) .WithCommand( ConfigureTrackedBrowserCommandName, - CommandStrings.ConfigureTrackedBrowserName, + BrowserCommandStrings.ConfigureTrackedBrowserName, async context => { try @@ -214,7 +212,7 @@ public static IResourceBuilder WithBrowserLogs( }, new CommandOptions { - Description = CommandStrings.ConfigureTrackedBrowserDescription, + Description = BrowserCommandStrings.ConfigureTrackedBrowserDescription, IconName = "Settings", IconVariant = IconVariant.Regular, UpdateState = context => @@ -227,7 +225,7 @@ public static IResourceBuilder WithBrowserLogs( }) .WithCommand( CaptureScreenshotCommandName, - CommandStrings.CaptureScreenshotName, + BrowserCommandStrings.CaptureScreenshotName, async context => { try @@ -266,7 +264,7 @@ public static IResourceBuilder WithBrowserLogs( }, new CommandOptions { - Description = CommandStrings.CaptureScreenshotDescription, + Description = BrowserCommandStrings.CaptureScreenshotDescription, IconName = "Camera", IconVariant = IconVariant.Regular, UpdateState = context => @@ -323,13 +321,13 @@ static Uri ResolveBrowserUrl(T resource) if (endpointAnnotation is null) { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsResourceMissingHttpEndpoint, resource.Name)); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsResourceMissingHttpEndpoint, resource.Name)); } var endpointReference = resource.GetEndpoint(endpointAnnotation.Name); if (!endpointReference.IsAllocated) { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsEndpointNotAllocated, endpointAnnotation.Name, resource.Name)); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsEndpointNotAllocated, endpointAnnotation.Name, resource.Name)); } return new Uri(endpointReference.Url, UriKind.Absolute); diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsCdpConnection.cs b/src/Aspire.Hosting.Browsers/BrowserLogsCdpConnection.cs similarity index 76% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsCdpConnection.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsCdpConnection.cs index f4a564e66f1..2e0c268a5a1 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserLogsCdpConnection.cs +++ b/src/Aspire.Hosting.Browsers/BrowserLogsCdpConnection.cs @@ -30,7 +30,7 @@ internal interface IBrowserLogsCdpConnection : IAsyncDisposable Task NavigateAsync(string sessionId, Uri url, CancellationToken cancellationToken); } -// Owns the browser-level websocket only. Protocol parsing stays in BrowserLogsCdpProtocol, while page lifecycle and +// Owns one browser-level CDP transport. Protocol parsing stays in BrowserLogsCdpProtocol, while page lifecycle and // reconnection policy stay in BrowserPageSession. internal sealed class BrowserLogsCdpConnection : IBrowserLogsCdpConnection { @@ -51,14 +51,14 @@ internal sealed class BrowserLogsCdpConnection : IBrowserLogsCdpConnection private readonly ConcurrentDictionary _pendingCommands = new(); private readonly Task _receiveLoop; private readonly SemaphoreSlim _sendLock = new(1, 1); - private readonly WebSocket _webSocket; + private readonly IBrowserLogsCdpTransport _transport; private long _nextCommandId; - private BrowserLogsCdpConnection(WebSocket webSocket, Func eventHandler, ILogger logger) + private BrowserLogsCdpConnection(IBrowserLogsCdpTransport transport, Func eventHandler, ILogger logger) { _eventHandler = eventHandler; _logger = logger; - _webSocket = webSocket; + _transport = transport; _receiveLoop = Task.Run(ReceiveLoopAsync); } @@ -90,7 +90,18 @@ internal static async Task ConnectAsync( // Keep-alives make transport failures show up in the receive loop instead of only on the next CDP command. connector.SetKeepAliveInterval(s_keepAliveInterval); await connector.ConnectAsync(webSocketUri, cancellationToken).ConfigureAwait(false); - return new BrowserLogsCdpConnection(connector.DetachConnectedWebSocket(), eventHandler, logger); + return Create( + new BrowserLogsWebSocketCdpTransport(connector.DetachConnectedWebSocket(), s_closeTimeout), + eventHandler, + logger); + } + + internal static BrowserLogsCdpConnection Create( + IBrowserLogsCdpTransport transport, + Func eventHandler, + ILogger logger) + { + return new BrowserLogsCdpConnection(transport, eventHandler, logger); } public Task CreateTargetAsync(CancellationToken cancellationToken) @@ -193,19 +204,10 @@ public async ValueTask DisposeAsync() try { - if (_webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived) - { - using var closeCts = new CancellationTokenSource(s_closeTimeout); - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposed", closeCts.Token).ConfigureAwait(false); - } + await _transport.DisposeAsync().ConfigureAwait(false); } catch { - _webSocket.Abort(); - } - finally - { - _webSocket.Dispose(); } try @@ -248,9 +250,9 @@ private async Task SendCommandAsync( await _sendLock.WaitAsync(sendCts.Token).ConfigureAwait(false); try { - // ClientWebSocket does not allow overlapping sends, so startup, reconnect, and shutdown all share - // this serialized path. - await _webSocket.SendAsync(payload, WebSocketMessageType.Text, endOfMessage: true, sendCts.Token).ConfigureAwait(false); + // Browser-level CDP transports are serialized so startup, reconnect, screenshot, and shutdown never + // interleave command frames on the same connection. + await _transport.SendAsync(payload, sendCts.Token).ConfigureAwait(false); } finally { @@ -271,32 +273,13 @@ private async Task SendCommandAsync( private async Task ReceiveLoopAsync() { - var buffer = new byte[16 * 1024]; - using var messageBuffer = new MemoryStream(); Exception? terminalException = null; try { - while (!_disposeCts.IsCancellationRequested && _webSocket.State is WebSocketState.Open or WebSocketState.CloseSent) + while (!_disposeCts.IsCancellationRequested) { - var result = await _webSocket.ReceiveAsync(buffer, _disposeCts.Token).ConfigureAwait(false); - if (result.MessageType == WebSocketMessageType.Close) - { - terminalException = CreateUnexpectedConnectionClosureException(result); - break; - } - - // Large CDP events can span multiple websocket frames. Buffer until EndOfMessage so protocol parsing - // always sees one complete JSON message, matching the frames observed from a real browser. - messageBuffer.Write(buffer, 0, result.Count); - if (!result.EndOfMessage) - { - continue; - } - - var frame = messageBuffer.ToArray(); - messageBuffer.SetLength(0); - + var frame = await _transport.ReceiveAsync(_disposeCts.Token).ConfigureAwait(false); _logger.LogTrace("Tracked browser protocol <- {Frame}", BrowserLogsCdpProtocol.DescribeFrame(frame)); try @@ -359,22 +342,6 @@ private async Task HandleFrameAsync(byte[] frame) } } - private static InvalidOperationException CreateUnexpectedConnectionClosureException(WebSocketReceiveResult result) - { - // Preserve the remote close details; they become the reconnect/resource-log diagnostics when CDP drops. - if (result.CloseStatus is { } closeStatus) - { - if (!string.IsNullOrWhiteSpace(result.CloseStatusDescription)) - { - return new InvalidOperationException($"Browser debug connection closed by the remote endpoint with status '{closeStatus}' ({(int)closeStatus}): {result.CloseStatusDescription}"); - } - - return new InvalidOperationException($"Browser debug connection closed by the remote endpoint with status '{closeStatus}' ({(int)closeStatus})."); - } - - return new InvalidOperationException("Browser debug connection closed by the remote endpoint without a close status."); - } - private interface IPendingCommand { void SetCanceled(); @@ -464,3 +431,144 @@ private ClientWebSocket GetWebSocket() return webSocket; } } + +// Transport abstraction for browser-level CDP frames. WebSocket uses complete text messages; Chromium pipe uses +// NUL-delimited JSON. Keeping framing here lets BrowserLogsCdpConnection own command correlation independent of launch +// transport. +internal interface IBrowserLogsCdpTransport : IAsyncDisposable +{ + Task SendAsync(ReadOnlyMemory frame, CancellationToken cancellationToken); + + Task ReceiveAsync(CancellationToken cancellationToken); +} + +internal sealed class BrowserLogsWebSocketCdpTransport(WebSocket webSocket, TimeSpan closeTimeout) : IBrowserLogsCdpTransport +{ + private readonly TimeSpan _closeTimeout = closeTimeout; + private readonly WebSocket _webSocket = webSocket; + + public async Task SendAsync(ReadOnlyMemory frame, CancellationToken cancellationToken) + { + await _webSocket.SendAsync(frame, WebSocketMessageType.Text, endOfMessage: true, cancellationToken).ConfigureAwait(false); + } + + public async Task ReceiveAsync(CancellationToken cancellationToken) + { + var buffer = new byte[16 * 1024]; + using var messageBuffer = new MemoryStream(); + + while (true) + { + var result = await _webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); + if (result.MessageType == WebSocketMessageType.Close) + { + throw CreateUnexpectedConnectionClosureException(result); + } + + // Large CDP events can span multiple websocket frames. Buffer until EndOfMessage so protocol parsing + // always sees one complete JSON message, matching the frames observed from a real browser. + messageBuffer.Write(buffer, 0, result.Count); + if (result.EndOfMessage) + { + return messageBuffer.ToArray(); + } + } + } + + public async ValueTask DisposeAsync() + { + try + { + if (_webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived) + { + using var closeCts = new CancellationTokenSource(_closeTimeout); + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposed", closeCts.Token).ConfigureAwait(false); + } + } + catch + { + _webSocket.Abort(); + } + finally + { + _webSocket.Dispose(); + } + } + + private static InvalidOperationException CreateUnexpectedConnectionClosureException(WebSocketReceiveResult result) + { + // Preserve the remote close details; they become the reconnect/resource-log diagnostics when CDP drops. + if (result.CloseStatus is { } closeStatus) + { + if (!string.IsNullOrWhiteSpace(result.CloseStatusDescription)) + { + return new InvalidOperationException($"Browser debug connection closed by the remote endpoint with status '{closeStatus}' ({(int)closeStatus}): {result.CloseStatusDescription}"); + } + + return new InvalidOperationException($"Browser debug connection closed by the remote endpoint with status '{closeStatus}' ({(int)closeStatus})."); + } + + return new InvalidOperationException("Browser debug connection closed by the remote endpoint without a close status."); + } +} + +internal sealed class BrowserLogsPipeCdpTransport(Stream readStream, Stream writeStream) : IBrowserLogsCdpTransport +{ + private const byte FrameTerminator = 0; + private const int ReadBufferSize = 16 * 1024; + private static readonly byte[] s_frameTerminator = [FrameTerminator]; + + private readonly byte[] _readBuffer = new byte[ReadBufferSize]; + private readonly Stream _readStream = readStream; + private readonly Stream _writeStream = writeStream; + private int _readBufferCount; + private int _readBufferOffset; + + public async Task SendAsync(ReadOnlyMemory frame, CancellationToken cancellationToken) + { + await _writeStream.WriteAsync(frame, cancellationToken).ConfigureAwait(false); + await _writeStream.WriteAsync(s_frameTerminator, cancellationToken).ConfigureAwait(false); + await _writeStream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task ReceiveAsync(CancellationToken cancellationToken) + { + using var messageBuffer = new MemoryStream(); + + while (true) + { + if (_readBufferOffset == _readBufferCount) + { + _readBufferOffset = 0; + _readBufferCount = await _readStream.ReadAsync(_readBuffer, cancellationToken).ConfigureAwait(false); + if (_readBufferCount == 0) + { + throw new EndOfStreamException("Browser debug pipe closed."); + } + } + + var terminatorIndex = Array.IndexOf(_readBuffer, FrameTerminator, _readBufferOffset, _readBufferCount - _readBufferOffset); + if (terminatorIndex >= 0) + { + messageBuffer.Write(_readBuffer, _readBufferOffset, terminatorIndex - _readBufferOffset); + _readBufferOffset = terminatorIndex + 1; + return messageBuffer.ToArray(); + } + + messageBuffer.Write(_readBuffer, _readBufferOffset, _readBufferCount - _readBufferOffset); + _readBufferOffset = _readBufferCount; + } + } + + public async ValueTask DisposeAsync() + { + try + { + await _writeStream.DisposeAsync().ConfigureAwait(false); + } + finally + { + await _readStream.DisposeAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Aspire.Hosting.Browsers/BrowserLogsCdpConnectionMultiplexer.cs b/src/Aspire.Hosting.Browsers/BrowserLogsCdpConnectionMultiplexer.cs new file mode 100644 index 00000000000..5a5515ddfed --- /dev/null +++ b/src/Aspire.Hosting.Browsers/BrowserLogsCdpConnectionMultiplexer.cs @@ -0,0 +1,237 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Aspire.Hosting; + +// Shares one browser-level CDP transport across multiple page sessions. Chromium pipe exposes one duplex connection per +// browser process, so pipe-backed hosts use lightweight per-session leases instead of opening one transport per tab. +internal sealed class BrowserLogsCdpConnectionMultiplexer : IAsyncDisposable +{ + private readonly object _lock = new(); + private readonly ILogger _logger; + private readonly IBrowserLogsCdpConnection _innerConnection; + private readonly Dictionary _subscriptions = []; + private int _disposed; + private long _nextSubscriptionId; + + public BrowserLogsCdpConnectionMultiplexer( + IBrowserLogsCdpTransport transport, + ILogger logger) + : this(eventHandler => BrowserLogsCdpConnection.Create(transport, eventHandler, logger), logger) + { + } + + internal BrowserLogsCdpConnectionMultiplexer( + Func, IBrowserLogsCdpConnection> connectionFactory, + ILogger logger) + { + _logger = logger; + _innerConnection = connectionFactory(DispatchEventAsync); + } + + public Task Completion => _innerConnection.Completion; + + public IBrowserLogsCdpConnection CreateConnection(Func eventHandler) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + ThrowIfInnerConnectionCompleted(); + + var subscriptionId = Interlocked.Increment(ref _nextSubscriptionId); + var subscription = new Subscription(subscriptionId, eventHandler); + + lock (_lock) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + ThrowIfInnerConnectionCompleted(); + _subscriptions.Add(subscriptionId, subscription); + } + + return new LeasedConnection(this, subscription); + } + + public async ValueTask DisposeAsync() + { + Subscription[] subscriptions; + + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + lock (_lock) + { + subscriptions = [.. _subscriptions.Values]; + _subscriptions.Clear(); + } + + foreach (var subscription in subscriptions) + { + subscription.SetCompleted(); + } + + await _innerConnection.DisposeAsync().ConfigureAwait(false); + } + + private async ValueTask DispatchEventAsync(BrowserLogsCdpProtocolEvent protocolEvent) + { + Subscription[] subscriptions; + + lock (_lock) + { + subscriptions = [.. _subscriptions.Values]; + } + + foreach (var subscription in subscriptions) + { + if (subscription.Completion.IsCompleted) + { + continue; + } + + try + { + await subscription.EventHandler(protocolEvent).ConfigureAwait(false); + } + catch (Exception ex) + { + var connectionException = new InvalidOperationException("Tracked browser CDP event handler failed.", ex); + if (TryRemoveSubscription(subscription)) + { + subscription.SetException(connectionException); + } + + _logger.LogError(ex, "Tracked browser CDP event handler failed for subscription '{SubscriptionId}'.", subscription.Id); + } + } + } + + private bool TryRemoveSubscription(Subscription subscription) + { + lock (_lock) + { + return _subscriptions.Remove(subscription.Id); + } + } + + private void ThrowIfInnerConnectionCompleted() + { + if (_innerConnection.Completion.IsCompleted) + { + throw new InvalidOperationException("Tracked browser CDP pipe is no longer active."); + } + } + + private ValueTask DisposeSubscriptionAsync(Subscription subscription) + { + if (TryRemoveSubscription(subscription)) + { + subscription.SetCompleted(); + } + + return ValueTask.CompletedTask; + } + + private sealed class LeasedConnection(BrowserLogsCdpConnectionMultiplexer owner, Subscription subscription) : IBrowserLogsCdpConnection + { + private readonly Task _completion = CompleteWhenLeaseOrInnerConnectionCompletesAsync(owner._innerConnection.Completion, subscription.Completion); + private int _disposed; + + public Task Completion => _completion; + + public Task CreateTargetAsync(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.CreateTargetAsync(cancellationToken); + } + + public Task GetTargetsAsync(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.GetTargetsAsync(cancellationToken); + } + + public Task AttachToTargetAsync(string targetId, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.AttachToTargetAsync(targetId, cancellationToken); + } + + public Task CloseTargetAsync(string targetId, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.CloseTargetAsync(targetId, cancellationToken); + } + + public Task EnableTargetDiscoveryAsync(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.EnableTargetDiscoveryAsync(cancellationToken); + } + + public Task EnablePageInstrumentationAsync(string sessionId, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.EnablePageInstrumentationAsync(sessionId, cancellationToken); + } + + public Task CaptureScreenshotAsync(string sessionId, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.CaptureScreenshotAsync(sessionId, cancellationToken); + } + + public Task NavigateAsync(string sessionId, Uri url, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return owner._innerConnection.NavigateAsync(sessionId, url, cancellationToken); + } + + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + await owner.DisposeSubscriptionAsync(subscription).ConfigureAwait(false); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + if (subscription.Completion.IsCompleted) + { + throw new InvalidOperationException("Tracked browser CDP connection subscription is no longer active."); + } + } + + private static async Task CompleteWhenLeaseOrInnerConnectionCompletesAsync(Task innerCompletion, Task leaseCompletion) + { + var completedTask = await Task.WhenAny(innerCompletion, leaseCompletion).ConfigureAwait(false); + await completedTask.ConfigureAwait(false); + } + } + + private sealed class Subscription(long id, Func eventHandler) + { + private readonly TaskCompletionSource _completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public long Id { get; } = id; + + public Func EventHandler { get; } = eventHandler; + + public Task Completion => _completionSource.Task; + + public void SetCompleted() + { + _completionSource.TrySetResult(); + } + + public void SetException(Exception exception) + { + _completionSource.TrySetException(exception); + } + } +} diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsCdpProtocol.cs b/src/Aspire.Hosting.Browsers/BrowserLogsCdpProtocol.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsCdpProtocol.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsCdpProtocol.cs diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsConfigurationManager.cs b/src/Aspire.Hosting.Browsers/BrowserLogsConfigurationManager.cs similarity index 86% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsConfigurationManager.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsConfigurationManager.cs index 4a1ed4267e4..33793277d94 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserLogsConfigurationManager.cs +++ b/src/Aspire.Hosting.Browsers/BrowserLogsConfigurationManager.cs @@ -3,10 +3,11 @@ #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only #pragma warning disable ASPIREUSERSECRETS001 // Type is for evaluation purposes only +#pragma warning disable ASPIREBROWSERLOGS001 // Type is for evaluation purposes only using System.Globalization; +using Aspire.Hosting.Browsers.Resources; using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Resources; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -36,18 +37,18 @@ public async Task ConfigureAsync(BrowserLogsResource resou if (!interactionService.IsAvailable) { - return CommandResults.Failure(CommandStrings.ConfigureTrackedBrowserInteractionUnavailable); + return CommandResults.Failure(BrowserCommandStrings.ConfigureTrackedBrowserInteractionUnavailable); } var currentConfiguration = resource.ResolveCurrentConfiguration(configuration, configurationStore); var inputs = CreateInputs(resource, currentConfiguration); var result = await interactionService.PromptInputsAsync( - CommandStrings.ConfigureTrackedBrowserName, - CommandStrings.ConfigureTrackedBrowserPromptMessage, + BrowserCommandStrings.ConfigureTrackedBrowserName, + BrowserCommandStrings.ConfigureTrackedBrowserPromptMessage, inputs, new InputsDialogInteractionOptions { - PrimaryButtonText = CommandStrings.ConfigureTrackedBrowserSaveButton, + PrimaryButtonText = BrowserCommandStrings.ConfigureTrackedBrowserSaveButton, ShowDismiss = true, EnableMessageMarkdown = true, ValidationCallback = context => ValidateInputsAsync(resource, context) @@ -70,10 +71,10 @@ public async Task ConfigureAsync(BrowserLogsResource resou var scopeName = selected.Scope == BrowserLogsConfigurationScope.Resource ? resource.ParentResource.Name - : CommandStrings.ConfigureTrackedBrowserGlobalScopeResult; + : BrowserCommandStrings.ConfigureTrackedBrowserGlobalScopeResult; var resultMessage = selected.SaveToUserSecrets - ? CommandStrings.ConfigureTrackedBrowserSaved - : CommandStrings.ConfigureTrackedBrowserApplied; + ? BrowserCommandStrings.ConfigureTrackedBrowserSaved + : BrowserCommandStrings.ConfigureTrackedBrowserApplied; return new ExecuteCommandResult { @@ -90,22 +91,22 @@ private List CreateInputs(BrowserLogsResource resource, Browse var scopeInput = new InteractionInput { Name = ScopeInputName, - Label = CommandStrings.ConfigureTrackedBrowserScopeLabel, + Label = BrowserCommandStrings.ConfigureTrackedBrowserScopeLabel, InputType = InputType.Choice, Required = true, Value = ResourceScopeValue, Options = [ - new(ResourceScopeValue, string.Format(CultureInfo.CurrentCulture, CommandStrings.ConfigureTrackedBrowserResourceScopeOption, resource.ParentResource.Name)), - new(GlobalScopeValue, CommandStrings.ConfigureTrackedBrowserGlobalScopeOption) + new(ResourceScopeValue, string.Format(CultureInfo.CurrentCulture, BrowserCommandStrings.ConfigureTrackedBrowserResourceScopeOption, resource.ParentResource.Name)), + new(GlobalScopeValue, BrowserCommandStrings.ConfigureTrackedBrowserGlobalScopeOption) ] }; var browserInput = new InteractionInput { Name = BrowserInputName, - Label = CommandStrings.ConfigureTrackedBrowserBrowserLabel, - Description = CommandStrings.ConfigureTrackedBrowserBrowserDescription, + Label = BrowserCommandStrings.ConfigureTrackedBrowserBrowserLabel, + Description = BrowserCommandStrings.ConfigureTrackedBrowserBrowserDescription, InputType = InputType.Choice, Required = true, AllowCustomChoice = true, @@ -116,7 +117,7 @@ private List CreateInputs(BrowserLogsResource resource, Browse var userDataModeInput = new InteractionInput { Name = UserDataModeInputName, - Label = CommandStrings.ConfigureTrackedBrowserUserDataModeLabel, + Label = BrowserCommandStrings.ConfigureTrackedBrowserUserDataModeLabel, InputType = InputType.Choice, Required = true, Value = currentConfiguration.UserDataMode.ToString(), @@ -130,8 +131,8 @@ private List CreateInputs(BrowserLogsResource resource, Browse var profileInput = new InteractionInput { Name = ProfileInputName, - Label = CommandStrings.ConfigureTrackedBrowserProfileLabel, - Description = CommandStrings.ConfigureTrackedBrowserProfileDescription, + Label = BrowserCommandStrings.ConfigureTrackedBrowserProfileLabel, + Description = BrowserCommandStrings.ConfigureTrackedBrowserProfileDescription, InputType = InputType.Choice, Required = false, AllowCustomChoice = true, @@ -158,12 +159,12 @@ private InteractionInput CreateSaveToUserSecretsInput() return new InteractionInput { Name = SaveToUserSecretsInputName, - Label = CommandStrings.ConfigureTrackedBrowserSaveToUserSecretsLabel, + Label = BrowserCommandStrings.ConfigureTrackedBrowserSaveToUserSecretsLabel, InputType = InputType.Boolean, Value = userSecretsManager.IsAvailable ? "true" : null, Description = userSecretsManager.IsAvailable - ? CommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured - : CommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured, + ? BrowserCommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured + : BrowserCommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured, EnableDescriptionMarkdown = true, Disabled = !userSecretsManager.IsAvailable }; @@ -172,9 +173,9 @@ private InteractionInput CreateSaveToUserSecretsInput() private static IReadOnlyList> GetBrowserOptions(string currentBrowser) { var options = new Dictionary(StringComparer.OrdinalIgnoreCase); - AddKnownBrowser("msedge", CommandStrings.ConfigureTrackedBrowserEdgeOption); - AddKnownBrowser("chrome", CommandStrings.ConfigureTrackedBrowserChromeOption); - AddKnownBrowser("chromium", CommandStrings.ConfigureTrackedBrowserChromiumOption); + AddKnownBrowser("msedge", BrowserCommandStrings.ConfigureTrackedBrowserEdgeOption); + AddKnownBrowser("chrome", BrowserCommandStrings.ConfigureTrackedBrowserChromeOption); + AddKnownBrowser("chromium", BrowserCommandStrings.ConfigureTrackedBrowserChromiumOption); if (!options.ContainsKey(currentBrowser)) { @@ -199,7 +200,7 @@ private void LoadProfileOptions(LoadInputContext context) var options = new Dictionary(StringComparer.OrdinalIgnoreCase) { - [BrowserDefaultProfileValue] = CommandStrings.ConfigureTrackedBrowserDefaultProfileOption + [BrowserDefaultProfileValue] = BrowserCommandStrings.ConfigureTrackedBrowserDefaultProfileOption }; var disableProfileInput = true; @@ -252,7 +253,7 @@ private static string FormatProfileOption(ChromiumBrowserProfile profile) { return string.Format( CultureInfo.CurrentCulture, - CommandStrings.ConfigureTrackedBrowserProfileOptionWithDisplayName, + BrowserCommandStrings.ConfigureTrackedBrowserProfileOptionWithDisplayName, profile.DirectoryName, profile.DisplayName); } @@ -267,14 +268,14 @@ private Task ValidateInputsAsync(BrowserLogsResource resource, InputsDialogValid var hasValidationErrors = false; if (string.IsNullOrWhiteSpace(browser.Value)) { - context.AddValidationError(browser, CommandStrings.ConfigureTrackedBrowserBrowserRequired); + context.AddValidationError(browser, BrowserCommandStrings.ConfigureTrackedBrowserBrowserRequired); hasValidationErrors = true; } var userDataMode = inputs[UserDataModeInputName]; if (!Enum.TryParse(userDataMode.Value, ignoreCase: true, out var parsedUserDataMode)) { - context.AddValidationError(userDataMode, CommandStrings.ConfigureTrackedBrowserUserDataModeRequired); + context.AddValidationError(userDataMode, BrowserCommandStrings.ConfigureTrackedBrowserUserDataModeRequired); hasValidationErrors = true; } @@ -283,14 +284,14 @@ private Task ValidateInputsAsync(BrowserLogsResource resource, InputsDialogValid !string.IsNullOrWhiteSpace(profile.Value) && !string.Equals(profile.Value, BrowserDefaultProfileValue, StringComparison.Ordinal)) { - context.AddValidationError(profile, CommandStrings.ConfigureTrackedBrowserProfileRequiresShared); + context.AddValidationError(profile, BrowserCommandStrings.ConfigureTrackedBrowserProfileRequiresShared); hasValidationErrors = true; } var saveToUserSecrets = inputs[SaveToUserSecretsInputName]; if (IsSaveToUserSecretsRequested(inputs) && !userSecretsManager.IsAvailable) { - context.AddValidationError(saveToUserSecrets, CommandStrings.ConfigureTrackedBrowserUserSecretsUnavailable); + context.AddValidationError(saveToUserSecrets, BrowserCommandStrings.ConfigureTrackedBrowserUserSecretsUnavailable); hasValidationErrors = true; } @@ -392,7 +393,7 @@ private void SaveValue(string key, string value) throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - CommandStrings.ConfigureTrackedBrowserSaveFailed, + BrowserCommandStrings.ConfigureTrackedBrowserSaveFailed, key)); } } @@ -404,7 +405,7 @@ private void DeleteValue(string key) throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - CommandStrings.ConfigureTrackedBrowserSaveFailed, + BrowserCommandStrings.ConfigureTrackedBrowserSaveFailed, key)); } } diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsConfigurationStore.cs b/src/Aspire.Hosting.Browsers/BrowserLogsConfigurationStore.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsConfigurationStore.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsConfigurationStore.cs diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsEventLogger.cs b/src/Aspire.Hosting.Browsers/BrowserLogsEventLogger.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsEventLogger.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsEventLogger.cs diff --git a/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcess.cs b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcess.cs new file mode 100644 index 00000000000..a8ce206d616 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcess.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting; + +internal interface IBrowserLogsPipeBrowserProcess : IAsyncDisposable +{ + int ProcessId { get; } + + Stream BrowserOutput { get; } + + Stream BrowserInput { get; } + + Task ProcessTask { get; } +} + +internal sealed class BrowserLogsPipeBrowserProcess( + int processId, + Stream browserOutput, + Stream browserInput, + Task processTask, + IBrowserLogsPipeBrowserProcessLifetime processLifetime) : IBrowserLogsPipeBrowserProcess +{ + private readonly Stream _browserInput = browserInput; + private readonly Stream _browserOutput = browserOutput; + private readonly IBrowserLogsPipeBrowserProcessLifetime _processLifetime = processLifetime; + private int _disposed; + + public int ProcessId { get; } = processId; + + public Stream BrowserOutput => _browserOutput; + + public Stream BrowserInput => _browserInput; + + public Task ProcessTask { get; } = processTask; + + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + try + { + await _browserInput.DisposeAsync().ConfigureAwait(false); + } + finally + { + try + { + await _browserOutput.DisposeAsync().ConfigureAwait(false); + } + finally + { + await _processLifetime.DisposeAsync().ConfigureAwait(false); + } + } + } +} + +internal interface IBrowserLogsPipeBrowserProcessLifetime +{ + ValueTask DisposeAsync(); +} diff --git a/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Unix.cs b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Unix.cs new file mode 100644 index 00000000000..964e6205a7c --- /dev/null +++ b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Unix.cs @@ -0,0 +1,375 @@ +// 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only + +using System.ComponentModel; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Aspire.Hosting; + +internal static partial class BrowserLogsPipeBrowserProcessLauncher +{ + private static BrowserLogsPipeBrowserProcess StartPosix(string executablePath, IReadOnlyList browserArguments) + { + var appToBrowser = PosixPipe.Invalid; + var browserToApp = PosixPipe.Invalid; + FileStream? browserInput = null; + FileStream? browserOutput = null; + + try + { + appToBrowser = CreatePosixPipe(); + browserToApp = CreatePosixPipe(); + // Chromium reserves fd 3 for browser input and fd 4 for browser output. pipe() can legally return either + // number for one of our source descriptors, so move accidental fd 3/4 allocations out of the way before + // posix_spawn file actions remap them. Without this, closing a "source" descriptor could accidentally close + // the final reserved descriptor the browser needs. + MoveReservedPipeDescriptors(ref appToBrowser); + MoveReservedPipeDescriptors(ref browserToApp); + + var arguments = CreatePipeArguments(browserArguments); + using var executablePathString = new NativeUtf8String(executablePath); + using var argv = NativeStringArray.Create([executablePath, .. arguments]); + using var environment = NativeStringArray.CreateEnvironment(); + using var fileActions = new PosixSpawnFileActions(); + + fileActions.AddDup2(appToBrowser.Read, 3); + fileActions.AddDup2(browserToApp.Write, 4); + fileActions.AddCloseIfNot(appToBrowser.Read, 3); + fileActions.AddCloseIfNot(browserToApp.Write, 4); + fileActions.AddClose(appToBrowser.Write); + fileActions.AddClose(browserToApp.Read); + + var spawnResult = posix_spawn( + out var processId, + executablePathString.Pointer, + fileActions.Pointer, + attrp: IntPtr.Zero, + argv.Pointer, + environment.Pointer); + if (spawnResult != 0) + { + throw CreatePosixSpawnException("posix_spawn", spawnResult); + } + + ClosePosixDescriptor(ref appToBrowser.Read); + ClosePosixDescriptor(ref browserToApp.Write); + + // After spawn, the parent owns the write side of appToBrowser and the read side of browserToApp. Wrap them + // in FileStream so the rest of BrowserLogs can treat pipe CDP like any other async stream transport. + browserInput = CreateFileStreamFromDescriptor(ref appToBrowser.Write, FileAccess.Write); + browserOutput = CreateFileStreamFromDescriptor(ref browserToApp.Read, FileAccess.Read); + var processTask = WaitForPosixProcessAsync(processId); + + return new BrowserLogsPipeBrowserProcess( + processId, + browserOutput, + browserInput, + processTask, + new PosixProcessLifetime(processId, processTask)); + } + catch + { + browserInput?.Dispose(); + browserOutput?.Dispose(); + appToBrowser.Dispose(); + browserToApp.Dispose(); + throw; + } + } + + private static FileStream CreateFileStreamFromDescriptor(ref int descriptor, FileAccess access) + { + var handle = new SafeFileHandle(new IntPtr(descriptor), ownsHandle: true); + descriptor = -1; + // Anonymous pipe descriptors are not opened with overlapped/async flags. FileStream still exposes Task-based + // methods over synchronous handles, but the constructor must be told the underlying handle is synchronous. + return new FileStream(handle, access, bufferSize: 16 * 1024, isAsync: false); + } + + private static PosixPipe CreatePosixPipe() + { + var descriptors = new int[2]; + if (pipe(descriptors) == -1) + { + throw CreatePosixException("pipe"); + } + + return new PosixPipe(descriptors[0], descriptors[1]); + } + + private static void MoveReservedPipeDescriptors(ref PosixPipe pipe) + { + pipe.Read = MoveReservedDescriptor(pipe.Read); + pipe.Write = MoveReservedDescriptor(pipe.Write); + } + + private static int MoveReservedDescriptor(int descriptor) + { + if (descriptor is not (3 or 4)) + { + return descriptor; + } + + var movedDescriptor = fcntl(descriptor, F_DUPFD, 5); + if (movedDescriptor == -1) + { + throw CreatePosixException("fcntl"); + } + + close(descriptor); + return movedDescriptor; + } + + private static async Task WaitForPosixProcessAsync(int processId) + { + return await Task.Run(() => + { + while (true) + { + var result = waitpid(processId, out var status, 0); + if (result == processId) + { + return new BrowserLogsProcessResult(GetPosixExitCode(status)); + } + + if (Marshal.GetLastPInvokeError() != EINTR) + { + throw CreatePosixException("waitpid"); + } + } + }).ConfigureAwait(false); + } + + private static int GetPosixExitCode(int status) + { + if ((status & 0x7f) == 0) + { + return (status >> 8) & 0xff; + } + + return 128 + (status & 0x7f); + } + + private static void ClosePosixDescriptor(ref int descriptor) + { + if (descriptor >= 0) + { + close(descriptor); + descriptor = -1; + } + } + + private static Win32Exception CreatePosixException(string operation) => + new(Marshal.GetLastPInvokeError(), $"Failed to invoke {operation} while starting tracked browser CDP pipe."); + + private static Win32Exception CreatePosixSpawnException(string operation, int errorCode) => + new(errorCode, $"Failed to invoke {operation} while starting tracked browser CDP pipe."); + + private sealed class PosixSpawnFileActions : IDisposable + { + // posix_spawn_file_actions_t is intentionally opaque. macOS defines it as a pointer-sized handle, while glibc + // and musl currently expose an 80-byte struct on 64-bit platforms. Allocate a conservative native buffer and let + // libc initialize/destroy its representation instead of running managed code in the child process. + private const int BufferSize = 256; + + public PosixSpawnFileActions() + { + Pointer = Marshal.AllocHGlobal(BufferSize); + + var result = posix_spawn_file_actions_init(Pointer); + if (result != 0) + { + Marshal.FreeHGlobal(Pointer); + Pointer = IntPtr.Zero; + throw CreatePosixSpawnException("posix_spawn_file_actions_init", result); + } + } + + public IntPtr Pointer { get; private set; } + + public void AddDup2(int descriptor, int targetDescriptor) + { + var result = posix_spawn_file_actions_adddup2(Pointer, descriptor, targetDescriptor); + if (result != 0) + { + throw CreatePosixSpawnException("posix_spawn_file_actions_adddup2", result); + } + } + + public void AddClose(int descriptor) + { + var result = posix_spawn_file_actions_addclose(Pointer, descriptor); + if (result != 0) + { + throw CreatePosixSpawnException("posix_spawn_file_actions_addclose", result); + } + } + + public void AddCloseIfNot(int descriptor, int targetDescriptor) + { + if (descriptor != targetDescriptor) + { + AddClose(descriptor); + } + } + + public void Dispose() + { + if (Pointer == IntPtr.Zero) + { + return; + } + + _ = posix_spawn_file_actions_destroy(Pointer); + Marshal.FreeHGlobal(Pointer); + Pointer = IntPtr.Zero; + } + } + + private struct PosixPipe(int read, int write) : IDisposable + { + public static PosixPipe Invalid => new(-1, -1); + + public int Read = read; + + public int Write = write; + + public void Dispose() + { + ClosePosixDescriptor(ref Read); + ClosePosixDescriptor(ref Write); + } + } + + private sealed class PosixProcessLifetime(int processId, Task processTask) : IBrowserLogsPipeBrowserProcessLifetime + { + public async ValueTask DisposeAsync() + { + if (processTask.IsCompleted) + { + return; + } + + sys_kill(processId, SIGINT); + try + { + await processTask.WaitAsync(s_processExitTimeout).ConfigureAwait(false); + } + catch (TimeoutException) + { + sys_kill(processId, SIGKILL); + await processTask.ConfigureAwait(false); + } + } + } + + private sealed class NativeUtf8String : IDisposable + { + public NativeUtf8String(string value) + { + Pointer = Marshal.StringToCoTaskMemUTF8(value); + } + + public IntPtr Pointer { get; } + + public void Dispose() + { + Marshal.FreeCoTaskMem(Pointer); + } + } + + private sealed class NativeStringArray : IDisposable + { + private readonly NativeUtf8String[] _strings; + + private NativeStringArray(NativeUtf8String[] strings, IntPtr pointer) + { + _strings = strings; + Pointer = pointer; + } + + public IntPtr Pointer { get; } + + public static NativeStringArray Create(IReadOnlyList values) + { + var strings = values.Select(static value => new NativeUtf8String(value)).ToArray(); + var pointer = Marshal.AllocHGlobal(IntPtr.Size * (strings.Length + 1)); + + for (var i = 0; i < strings.Length; i++) + { + Marshal.WriteIntPtr(pointer, i * IntPtr.Size, strings[i].Pointer); + } + + Marshal.WriteIntPtr(pointer, strings.Length * IntPtr.Size, IntPtr.Zero); + return new NativeStringArray(strings, pointer); + } + + public static NativeStringArray CreateEnvironment() + { + var environmentVariables = Environment.GetEnvironmentVariables(); + var values = new List(environmentVariables.Count); + foreach (System.Collections.DictionaryEntry variable in environmentVariables) + { + if (variable.Key is string key && variable.Value is string value) + { + values.Add($"{key}={value}"); + } + } + + return Create(values); + } + + public void Dispose() + { + Marshal.FreeHGlobal(Pointer); + foreach (var value in _strings) + { + value.Dispose(); + } + } + } + + private const int EINTR = 4; + private const int F_DUPFD = 0; + private const int SIGINT = 2; + private const int SIGKILL = 9; + + [LibraryImport("libc", SetLastError = true)] + private static partial int close(int fd); + + [LibraryImport("libc", SetLastError = true)] + private static partial int fcntl(int fd, int cmd, int arg); + + [LibraryImport("libc", SetLastError = true)] + private static partial int pipe([Out] int[] pipefd); + + [LibraryImport("libc")] + private static partial int posix_spawn( + out int pid, + IntPtr path, + IntPtr fileActions, + IntPtr attrp, + IntPtr argv, + IntPtr envp); + + [LibraryImport("libc")] + private static partial int posix_spawn_file_actions_addclose(IntPtr fileActions, int descriptor); + + [LibraryImport("libc")] + private static partial int posix_spawn_file_actions_adddup2(IntPtr fileActions, int descriptor, int targetDescriptor); + + [LibraryImport("libc")] + private static partial int posix_spawn_file_actions_destroy(IntPtr fileActions); + + [LibraryImport("libc")] + private static partial int posix_spawn_file_actions_init(IntPtr fileActions); + + [LibraryImport("libc", SetLastError = true, EntryPoint = "kill")] + private static partial int sys_kill(int pid, int sig); + + [LibraryImport("libc", SetLastError = true)] + private static partial int waitpid(int pid, out int status, int options); +} diff --git a/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Windows.cs b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Windows.cs new file mode 100644 index 00000000000..8f23b8a8a72 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.Windows.cs @@ -0,0 +1,464 @@ +// 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only + +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace Aspire.Hosting; + +internal static partial class BrowserLogsPipeBrowserProcessLauncher +{ + private static BrowserLogsPipeBrowserProcess StartWindows(string executablePath, IReadOnlyList browserArguments) + { + // Parent writes CDP commands to appToBrowser; the browser reads the client end. Parent reads responses/events + // from browserToApp; the browser writes the client end. AnonymousPipeServerStream makes the client handles + // inheritable, but CreateWindowsProcess below restricts inheritance to only those two handles. + var appToBrowser = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable); + var browserToApp = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable); + SafeWaitHandle? jobHandle = null; + SafeWaitHandle? processHandle = null; + try + { + var browserReadHandle = appToBrowser.GetClientHandleAsString(); + var browserWriteHandle = browserToApp.GetClientHandleAsString(); + var arguments = CreatePipeArguments(browserArguments); + // Chromium expects the child read handle first and the child write handle second. These are raw Win32 handle + // values, not file descriptor numbers, and Chromium opens them before starting DevTools pipe IO. + arguments.Add($"--remote-debugging-io-pipes={browserReadHandle},{browserWriteHandle}"); + + var inheritedHandles = new[] + { + new IntPtr(long.Parse(browserReadHandle, CultureInfo.InvariantCulture)), + new IntPtr(long.Parse(browserWriteHandle, CultureInfo.InvariantCulture)) + }; + + var processInfo = CreateWindowsProcess(executablePath, arguments, inheritedHandles); + processHandle = new SafeWaitHandle(processInfo.ProcessHandle, ownsHandle: true); + CloseWindowsHandle(processInfo.ThreadHandle); + // A Windows job with KILL_ON_JOB_CLOSE gives pipe-created browsers the same "owned by the AppHost" + // behavior even if the AppHost process exits before managed cleanup can run. Assigning can fail when a + // parent job forbids nested jobs, so this remains best-effort and normal DisposeAsync cleanup is still + // the primary path. + jobHandle = TryCreateKillOnCloseJob(processHandle); + + appToBrowser.DisposeLocalCopyOfClientHandle(); + browserToApp.DisposeLocalCopyOfClientHandle(); + + var processTask = WaitForWindowsProcessAsync(processHandle); + return new BrowserLogsPipeBrowserProcess( + processInfo.ProcessId, + browserToApp, + appToBrowser, + processTask, + new WindowsProcessLifetime(processInfo.ProcessId, processHandle, jobHandle, processTask)); + } + catch + { + jobHandle?.Dispose(); + processHandle?.Dispose(); + appToBrowser.Dispose(); + browserToApp.Dispose(); + throw; + } + } + + private static WindowsProcessInfo CreateWindowsProcess(string executablePath, IReadOnlyList arguments, IntPtr[] inheritedHandles) + { + var commandLine = Marshal.StringToHGlobalUni(BuildWindowsCommandLine(executablePath, arguments)); + var attributeListSize = UIntPtr.Zero; + // STARTUPINFOEX + PROC_THREAD_ATTRIBUTE_HANDLE_LIST lets us turn on handle inheritance while limiting it to the + // two CDP pipe handles. That avoids the broad "all inheritable handles leak into Chromium" behavior of plain + // CreateProcess(..., inheritHandles: true). + _ = InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref attributeListSize); + var attributeList = Marshal.AllocHGlobal((nint)attributeListSize.ToUInt64()); + var handleList = Marshal.AllocHGlobal(IntPtr.Size * inheritedHandles.Length); + var startupInfoPointer = IntPtr.Zero; + + try + { + if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize)) + { + throw CreateWindowsException("InitializeProcThreadAttributeList"); + } + + for (var i = 0; i < inheritedHandles.Length; i++) + { + Marshal.WriteIntPtr(handleList, i * IntPtr.Size, inheritedHandles[i]); + } + + if (!UpdateProcThreadAttribute( + attributeList, + 0, + s_procThreadAttributeHandleList, + handleList, + (UIntPtr)(uint)(IntPtr.Size * inheritedHandles.Length), + IntPtr.Zero, + IntPtr.Zero)) + { + throw CreateWindowsException("UpdateProcThreadAttribute"); + } + + var startupInfo = new STARTUPINFOEX + { + StartupInfo = new STARTUPINFO + { + cb = Marshal.SizeOf() + }, + lpAttributeList = attributeList + }; + startupInfoPointer = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(startupInfo, startupInfoPointer, fDeleteOld: false); + + if (!CreateProcessW( + lpApplicationName: IntPtr.Zero, + lpCommandLine: commandLine, + lpProcessAttributes: IntPtr.Zero, + lpThreadAttributes: IntPtr.Zero, + bInheritHandles: true, + dwCreationFlags: EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW, + lpEnvironment: IntPtr.Zero, + lpCurrentDirectory: IntPtr.Zero, + lpStartupInfo: startupInfoPointer, + lpProcessInformation: out var processInformation)) + { + throw CreateWindowsException("CreateProcessW"); + } + + return new WindowsProcessInfo(processInformation.dwProcessId, processInformation.hProcess, processInformation.hThread); + } + finally + { + DeleteProcThreadAttributeList(attributeList); + Marshal.FreeHGlobal(startupInfoPointer); + Marshal.FreeHGlobal(attributeList); + Marshal.FreeHGlobal(handleList); + Marshal.FreeHGlobal(commandLine); + } + } + + internal static string BuildWindowsCommandLine(string executablePath, IReadOnlyList arguments) + { + var builder = new StringBuilder(); + AppendWindowsCommandLineArgument(builder, executablePath); + + foreach (var argument in arguments) + { + builder.Append(' '); + AppendWindowsCommandLineArgument(builder, argument); + } + + return builder.ToString(); + } + + // Adapted from dotnet/runtime PasteArguments.AppendArgument so CreateProcess receives the same argv Chromium expects. + private static void AppendWindowsCommandLineArgument(StringBuilder builder, string argument) + { + if (argument.Length != 0 && !argument.AsSpan().ContainsAny(' ', '\t', '"')) + { + builder.Append(argument); + return; + } + + builder.Append('"'); + + var index = 0; + while (index < argument.Length) + { + var character = argument[index++]; + if (character == '\\') + { + var backslashCount = 1; + while (index < argument.Length && argument[index] == '\\') + { + index++; + backslashCount++; + } + + if (index == argument.Length) + { + builder.Append('\\', backslashCount * 2); + } + else if (argument[index] == '"') + { + builder.Append('\\', backslashCount * 2 + 1); + builder.Append('"'); + index++; + } + else + { + builder.Append('\\', backslashCount); + } + + continue; + } + + if (character == '"') + { + builder.Append('\\'); + builder.Append('"'); + continue; + } + + builder.Append(character); + } + + builder.Append('"'); + } + + private static async Task WaitForWindowsProcessAsync(SafeWaitHandle processHandle) + { + return await Task.Run(() => + { + var waitResult = WaitForSingleObject(processHandle.DangerousGetHandle(), INFINITE); + if (waitResult != WAIT_OBJECT_0) + { + throw CreateWindowsException("WaitForSingleObject"); + } + + if (!GetExitCodeProcess(processHandle.DangerousGetHandle(), out var exitCode)) + { + throw CreateWindowsException("GetExitCodeProcess"); + } + + return new BrowserLogsProcessResult(unchecked((int)exitCode)); + }).ConfigureAwait(false); + } + + private static Win32Exception CreateWindowsException(string operation) => + new(Marshal.GetLastWin32Error(), $"Failed to invoke {operation} while starting tracked browser CDP pipe."); + + private static void CloseWindowsHandle(IntPtr handle) + { + if (handle != IntPtr.Zero && !CloseHandle(handle)) + { + throw CreateWindowsException("CloseHandle"); + } + } + + private static void TryKillProcessTree(int processId) + { + try + { + using var process = Process.GetProcessById(processId); + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch (ArgumentException) + { + } + catch (InvalidOperationException) + { + } + } + + private static SafeWaitHandle? TryCreateKillOnCloseJob(SafeWaitHandle processHandle) + { + var job = CreateJobObjectW(IntPtr.Zero, lpName: null); + if (job == IntPtr.Zero) + { + return null; + } + + var jobHandle = new SafeWaitHandle(job, ownsHandle: true); + var jobInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + } + }; + var jobInfoSize = Marshal.SizeOf(); + var jobInfoPointer = Marshal.AllocHGlobal(jobInfoSize); + + try + { + Marshal.StructureToPtr(jobInfo, jobInfoPointer, fDeleteOld: false); + if (!SetInformationJobObject(jobHandle.DangerousGetHandle(), JobObjectExtendedLimitInformation, jobInfoPointer, (uint)jobInfoSize) || + !AssignProcessToJobObject(jobHandle.DangerousGetHandle(), processHandle.DangerousGetHandle())) + { + jobHandle.Dispose(); + return null; + } + } + finally + { + Marshal.FreeHGlobal(jobInfoPointer); + } + + return jobHandle; + } + + private readonly record struct WindowsProcessInfo(int ProcessId, IntPtr ProcessHandle, IntPtr ThreadHandle); + + private sealed class WindowsProcessLifetime(int processId, SafeWaitHandle processHandle, SafeWaitHandle? jobHandle, Task processTask) : IBrowserLogsPipeBrowserProcessLifetime + { + public async ValueTask DisposeAsync() + { + try + { + if (!processTask.IsCompleted) + { + TryKillProcessTree(processId); + await processTask.WaitAsync(s_processExitTimeout).ConfigureAwait(false); + } + } + catch (TimeoutException) + { + TryKillProcessTree(processId); + await processTask.ConfigureAwait(false); + } + finally + { + jobHandle?.Dispose(); + processHandle.Dispose(); + } + } + } + + private const int CREATE_NO_WINDOW = 0x08000000; + private const int EXTENDED_STARTUPINFO_PRESENT = 0x00080000; + private const uint INFINITE = 0xffffffff; + private const int JobObjectExtendedLimitInformation = 9; + private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; + private static readonly IntPtr s_procThreadAttributeHandleList = 0x00020002; + private const uint WAIT_OBJECT_0 = 0x00000000; + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool CloseHandle(IntPtr hObject); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess); + + [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + private static partial IntPtr CreateJobObjectW(IntPtr lpJobAttributes, string? lpName); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool CreateProcessW( + IntPtr lpApplicationName, + IntPtr lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + [MarshalAs(UnmanagedType.Bool)] + bool bInheritHandles, + int dwCreationFlags, + IntPtr lpEnvironment, + IntPtr lpCurrentDirectory, + IntPtr lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [LibraryImport("kernel32.dll", SetLastError = true)] + private static partial void DeleteProcThreadAttributeList(IntPtr lpAttributeList); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref UIntPtr lpSize); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetInformationJobObject(IntPtr hJob, int jobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool UpdateProcThreadAttribute( + IntPtr lpAttributeList, + int dwFlags, + IntPtr attribute, + IntPtr lpValue, + UIntPtr cbSize, + IntPtr lpPreviousValue, + IntPtr lpReturnSize); + + [LibraryImport("kernel32.dll", SetLastError = true)] + private static partial uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IO_COUNTERS + { + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + private struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public UIntPtr Affinity; + public uint PriorityClass; + public uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct STARTUPINFO + { + public int cb; + public string? lpReserved; + public string? lpDesktop; + public string? lpTitle; + public int dwX; + public int dwY; + public int dwXSize; + public int dwYSize; + public int dwXCountChars; + public int dwYCountChars; + public int dwFillAttribute; + public int dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct STARTUPINFOEX + { + public STARTUPINFO StartupInfo; + public IntPtr lpAttributeList; + } +} diff --git a/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.cs b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.cs new file mode 100644 index 00000000000..16750255e20 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/BrowserLogsPipeBrowserProcessLauncher.cs @@ -0,0 +1,31 @@ +// 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only + +namespace Aspire.Hosting; + +// Starts Chromium with a private CDP pipe. This cannot use ProcessStartInfo today because the repo's target frameworks +// do not expose a supported way to map arbitrary child handles/fds. Chromium's pipe protocol has platform-specific +// launch contracts: +// - POSIX: the child must see the browser-input pipe at fd 3 and browser-output pipe at fd 4. +// - Windows: Chromium can adopt explicit inherited handles supplied through --remote-debugging-io-pipes=,. +internal static partial class BrowserLogsPipeBrowserProcessLauncher +{ + private const string RemoteDebuggingPipeArgument = "--remote-debugging-pipe"; + private static readonly TimeSpan s_processExitTimeout = TimeSpan.FromSeconds(5); + + public static IBrowserLogsPipeBrowserProcess Start( + string executablePath, + IReadOnlyList browserArguments) + { + return OperatingSystem.IsWindows() + ? StartWindows(executablePath, browserArguments) + : StartPosix(executablePath, browserArguments); + } + + internal static List CreatePipeArguments(IReadOnlyList browserArguments) + { + return [.. browserArguments, RemoteDebuggingPipeArgument]; + } +} diff --git a/src/Aspire.Hosting.Browsers/BrowserLogsProcessResult.cs b/src/Aspire.Hosting.Browsers/BrowserLogsProcessResult.cs new file mode 100644 index 00000000000..fc141346be5 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/BrowserLogsProcessResult.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting; + +internal sealed class BrowserLogsProcessResult(int exitCode) +{ + public int ExitCode { get; } = exitCode; +} diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsResource.cs b/src/Aspire.Hosting.Browsers/BrowserLogsResource.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsResource.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsResource.cs diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsRunningSession.cs b/src/Aspire.Hosting.Browsers/BrowserLogsRunningSession.cs similarity index 95% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsRunningSession.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsRunningSession.cs index f0f789d5952..f7bab646606 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserLogsRunningSession.cs +++ b/src/Aspire.Hosting.Browsers/BrowserLogsRunningSession.cs @@ -13,7 +13,7 @@ internal interface IBrowserLogsRunningSession string BrowserExecutable { get; } - Uri BrowserDebugEndpoint { get; } + Uri? BrowserDebugEndpoint { get; } BrowserHostOwnership BrowserHostOwnership { get; } @@ -130,7 +130,7 @@ private BrowserLogsRunningSession( public string BrowserExecutable => _browserExecutable ?? throw new InvalidOperationException("Browser executable is not available before the session starts."); - public Uri BrowserDebugEndpoint => _browserEndpoint ?? throw new InvalidOperationException("Browser debugging endpoint is not available before the session starts."); + public Uri? BrowserDebugEndpoint => _browserEndpoint; public BrowserHostOwnership BrowserHostOwnership => _browserHostOwnership ?? throw new InvalidOperationException("Browser host ownership is not available before the session starts."); @@ -178,9 +178,11 @@ public async Task CaptureScreenshotAsync(CancellationToken cancellationT var pageSession = _pageSession ?? throw new InvalidOperationException("Browser page session is not available."); var result = await pageSession.CaptureScreenshotAsync(cancellationToken).ConfigureAwait(false); + var data = result.Data ?? throw new InvalidOperationException("Tracked browser screenshot capture returned no image data."); + try { - return Convert.FromBase64String(result.Data!); + return Convert.FromBase64String(data); } catch (FormatException ex) { @@ -190,7 +192,7 @@ public async Task CaptureScreenshotAsync(CancellationToken cancellationT public async Task StopAsync(CancellationToken cancellationToken) { - _stopCts.Cancel(); + try { _stopCts.Cancel(); } catch (ObjectDisposedException) { } // Stopping a dashboard browser-log session should close only the page target it created. The shared browser // process/window is released through the lease and may stay alive while other resource sessions are still active. @@ -236,7 +238,7 @@ private async Task InitializeAsync(CancellationToken cancellationToken) _sessionId, browserHost.Ownership, _browserExecutable, - _browserEndpoint); + FormatDebugEndpoint(_browserEndpoint)); try { @@ -338,6 +340,9 @@ private async Task DisposePageSessionAsync() } } + private static string FormatDebugEndpoint(Uri? browserDebugEndpoint) => + browserDebugEndpoint?.ToString() ?? "private CDP pipe"; + private async Task DisposeBrowserHostLeaseAsync() { var browserHostLease = _browserHostLease; diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsSessionManager.cs b/src/Aspire.Hosting.Browsers/BrowserLogsSessionManager.cs similarity index 94% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsSessionManager.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsSessionManager.cs index 629c5bac533..5646c0a421a 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserLogsSessionManager.cs +++ b/src/Aspire.Hosting.Browsers/BrowserLogsSessionManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only +#pragma warning disable ASPIREBROWSERLOGS001 // Type is for evaluation purposes only using System.Collections.Concurrent; using System.Collections.Immutable; @@ -88,7 +89,6 @@ public async Task StartSessionAsync(BrowserLogsResource resource, BrowserConfigu } resourceState.LastError = null; resourceState.LastProfile = configuration.Profile; - var resourceLogger = _resourceLoggerService.GetLogger(resourceName); resourceLogger.LogInformation("[{SessionId}] Opening tracked browser for '{Url}' using '{Browser}'.", sessionId, url, configuration.Browser); @@ -143,9 +143,8 @@ await PublishResourceSnapshotAsync( await HandleSessionCompletedAsync(resource, resourceName, resourceState, session.SessionId, exitCode, error).ConfigureAwait(false); }); - // The session snapshot intentionally stores both browser-level and page-level endpoints. In the dashboard, - // the browser endpoint explains what process was adopted/owned, while the page endpoint lets a developer - // inspect the exact target that is producing resource logs. + // WebSocket-backed sessions expose browser-level and page-level CDP endpoints for inspection. Pipe-backed + // sessions intentionally expose only a transport description because their CDP connection is private. resourceState.ActiveSessions[session.SessionId] = new ActiveBrowserSession( session.SessionId, configuration.AppHostKey, @@ -383,15 +382,15 @@ private Task PublishResourceSnapshotAsync( var healthReports = GetHealthReports(resourceState, pendingSession); var propertyUpdates = GetPropertyUpdates(resourceState); - return _resourceNotificationService.PublishUpdateAsync(resource, resourceName, snapshot => snapshot with - { - StartTimeStamp = startTimeStamp ?? snapshot.StartTimeStamp, - StopTimeStamp = resourceState.ActiveSessions.Count > 0 || pendingSession is not null ? null : stopTimeStamp, - ExitCode = resourceState.ActiveSessions.Count > 0 || pendingSession is not null ? null : exitCode, - State = new ResourceStateSnapshot(stateText, stateStyle), - Properties = UpdateProperties(snapshot.Properties, resourceState, propertyUpdates), - HealthReports = healthReports - }); + return _resourceNotificationService.PublishUpdateAsync(resource, resourceName, snapshot => + (snapshot with + { + StartTimeStamp = startTimeStamp ?? snapshot.StartTimeStamp, + StopTimeStamp = resourceState.ActiveSessions.Count > 0 || pendingSession is not null ? null : stopTimeStamp, + ExitCode = resourceState.ActiveSessions.Count > 0 || pendingSession is not null ? null : exitCode, + State = new ResourceStateSnapshot(stateText, stateStyle), + Properties = UpdateProperties(snapshot.Properties, resourceState, propertyUpdates) + }).WithHealthReports(healthReports)); } private ImmutableArray GetHealthReports(ResourceSessionState resourceState, PendingBrowserSession? pendingSession) @@ -546,7 +545,7 @@ private static string FormatBrowserSessions(IEnumerable se session.StartedAt, session.TargetUrl.ToString(), session.BrowserHostOwnership, - session.BrowserDebugEndpoint.ToString(), + FormatDebugEndpoint(session.BrowserDebugEndpoint), GetPageDebugEndpoint(session.BrowserDebugEndpoint, session.TargetId), session.TargetId)) .ToArray(); @@ -554,8 +553,13 @@ private static string FormatBrowserSessions(IEnumerable se return JsonSerializer.Serialize(activeSessions, s_browserSessionPropertyJsonOptions); } - private static string GetPageDebugEndpoint(Uri browserDebugEndpoint, string targetId) + private static string? GetPageDebugEndpoint(Uri? browserDebugEndpoint, string targetId) { + if (browserDebugEndpoint is null) + { + return null; + } + var builder = new UriBuilder(browserDebugEndpoint) { Path = $"/devtools/page/{targetId}" @@ -585,6 +589,7 @@ private sealed class ResourceSessionState public string? LastBrowser { get; set; } public string? LastProfile { get; set; } + } private static string FormatProcessId(int? processId) => @@ -596,7 +601,7 @@ private sealed record ActiveBrowserSession( string Browser, string BrowserExecutable, string? Profile, - Uri BrowserDebugEndpoint, + Uri? BrowserDebugEndpoint, string BrowserHostOwnership, int? ProcessId, DateTime StartedAt, @@ -615,9 +620,12 @@ private sealed record BrowserSessionPropertyValue( string TargetUrl, string BrowserHostOwnership, string CdpEndpoint, - string PageCdpEndpoint, + string? PageCdpEndpoint, string TargetId); + private static string FormatDebugEndpoint(Uri? browserDebugEndpoint) => + browserDebugEndpoint?.ToString() ?? "pipe"; + private sealed record PendingBrowserSession( string SessionId, DateTime StartedAt, diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserLogsUserDataDirectory.cs b/src/Aspire.Hosting.Browsers/BrowserLogsUserDataDirectory.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/BrowserLogsUserDataDirectory.cs rename to src/Aspire.Hosting.Browsers/BrowserLogsUserDataDirectory.cs diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserPageSession.cs b/src/Aspire.Hosting.Browsers/BrowserPageSession.cs similarity index 95% rename from src/Aspire.Hosting/BrowserLogs/BrowserPageSession.cs rename to src/Aspire.Hosting.Browsers/BrowserPageSession.cs index ad295878575..3a493fb62cf 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserPageSession.cs +++ b/src/Aspire.Hosting.Browsers/BrowserPageSession.cs @@ -7,7 +7,6 @@ namespace Aspire.Hosting; // Factory for browser-level CDP connections. internal delegate Task BrowserLogsCdpConnectionFactory( - Uri webSocketUri, Func eventHandler, ILogger logger, CancellationToken cancellationToken); @@ -18,7 +17,7 @@ internal delegate Task BrowserLogsCdpConnectionFactor // reconnection loop. internal sealed class BrowserPageSession : IBrowserPageSession { - // Keep reconnects quick and local to transient websocket loss. A 200 ms cadence gives the browser a few chances to + // Keep reconnects quick and local to transient CDP connection loss. A 200 ms cadence gives the browser a few chances to // recover within the 5 s window without making the dashboard look healthy after the page is truly gone. Target close // uses a shorter 3 s budget because disposal should not block AppHost shutdown on an unresponsive browser. private static readonly TimeSpan s_connectionRecoveryDelay = TimeSpan.FromMilliseconds(200); @@ -32,7 +31,7 @@ internal sealed class BrowserPageSession : IBrowserPageSession private readonly IBrowserHost _host; // Serializes every operation that replaces, disposes, or sends a command through the current CDP connection. // Without this, screenshot capture can read a live connection reference while reconnect/dispose tears down that - // same websocket underneath it. + // same connection underneath it. private readonly SemaphoreSlim _connectionLock = new(1, 1); private readonly ILogger _logger; private readonly bool _reuseInitialBlankTarget; @@ -141,8 +140,7 @@ public static async Task StartAsync( sessionId, url, connectionDiagnostics, - static async (webSocketUri, eventHandler, logger, cancellationToken) => - await BrowserLogsCdpConnection.ConnectAsync(webSocketUri, eventHandler, logger, cancellationToken).ConfigureAwait(false), + host.CreateCdpConnectionAsync, eventHandler, logger, timeProvider, @@ -187,7 +185,7 @@ public async ValueTask DisposeAsync() // Cancel first so any in-flight screenshot command holding _connectionLock is interrupted before disposal waits // for the lock. Once the lock is acquired, no new capture/reconnect can use the connection while the target is - // being closed and the websocket is being disposed. + // being closed and the connection is being disposed. await _connectionLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { @@ -231,7 +229,7 @@ public async ValueTask DisposeAsync() private async Task ConnectAsync(bool createTarget, CancellationToken cancellationToken) { - // ConnectAsync is used for startup and reconnect. It swaps the current websocket, target attachment, and target + // ConnectAsync is used for startup and reconnect. It swaps the current CDP connection, target attachment, and target // session id as one critical section so command callers never observe a half-attached page session. await _connectionLock.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -239,9 +237,9 @@ private async Task ConnectAsync(bool createTarget, CancellationToken cancellatio { await DisposeConnectionCoreAsync().ConfigureAwait(false); - _connection = await _connectionFactory(_host.DebugEndpoint, HandleEventAsync, _logger, cancellationToken).ConfigureAwait(false); + _connection = await _connectionFactory(HandleEventAsync, _logger, cancellationToken).ConfigureAwait(false); // Target discovery must be re-enabled for every browser-level connection, including reconnects. The - // subscription is attached to this websocket, not to the browser process, and it is what makes Chromium emit + // subscription is attached to this connection, not to the browser process, and it is what makes Chromium emit // targetDestroyed/targetCrashed/detachedFromTarget events that tell us whether the tracked tab is gone. await _connection.EnableTargetDiscoveryAsync(cancellationToken).ConfigureAwait(false); @@ -255,7 +253,7 @@ private async Task ConnectAsync(bool createTarget, CancellationToken cancellatio throw new InvalidOperationException("Tracked browser target id is not available."); } - // Reconnects reuse the existing target id. A transient websocket drop does not necessarily close the browser + // Reconnects reuse the existing target id. A transient CDP connection drop does not necessarily close the browser // tab, so recovering should reattach to the same page instead of opening a duplicate tab in the user's browser. var attachToTargetResult = await _connection.AttachToTargetAsync(_targetId, cancellationToken).ConfigureAwait(false); _targetSessionId = attachToTargetResult.SessionId @@ -382,7 +380,7 @@ private async Task TryReconnectAsync(Exception connectionError) { await DisposeConnectionAsync().ConfigureAwait(false); - // In a real browser the CDP websocket can disappear briefly while the tab keeps running (for example during + // In a real browser the CDP connection can disappear briefly while the tab keeps running (for example during // browser hiccups or network stack resets). Give it a short recovery window so logs continue without opening // another tab, but fail fast enough that the dashboard does not look healthy after the target is truly gone. var reconnectDeadline = _timeProvider.GetUtcNow() + s_connectionRecoveryTimeout; diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserUserDataMode.cs b/src/Aspire.Hosting.Browsers/BrowserUserDataMode.cs similarity index 72% rename from src/Aspire.Hosting/BrowserLogs/BrowserUserDataMode.cs rename to src/Aspire.Hosting.Browsers/BrowserUserDataMode.cs index 6b1df8e5838..572623d3257 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserUserDataMode.cs +++ b/src/Aspire.Hosting.Browsers/BrowserUserDataMode.cs @@ -1,11 +1,14 @@ // 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; + namespace Aspire.Hosting; /// /// Selects the Chromium user data directory used by tracked browser sessions. /// +[Experimental("ASPIREBROWSERLOGS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public enum BrowserUserDataMode { /// @@ -14,9 +17,8 @@ public enum BrowserUserDataMode /// /// /// The directory lives under a well-known path (for example %LocalAppData%\Aspire\BrowserData\shared\<browser> - /// on Windows). When multiple AppHosts run concurrently, the second AppHost adopts the existing browser via the - /// Chrome DevTools Protocol instead of launching a new one. The browser is never closed automatically when an - /// AppHost exits. + /// on Windows). The directory is persistent across AppHost restarts, but the default tracked browser process is + /// owned by the AppHost that launched it. /// Shared, @@ -26,8 +28,8 @@ public enum BrowserUserDataMode /// /// /// The directory is keyed on a stable hash of the AppHost project path (for example - /// %LocalAppData%\Aspire\BrowserData\isolated\<hash>\<browser> on Windows). The browser is never - /// closed automatically when the AppHost exits. + /// %LocalAppData%\Aspire\BrowserData\isolated\<hash>\<browser> on Windows). The directory persists across + /// runs, but the default tracked browser process is owned by the AppHost that launched it. /// Isolated, } diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserUserDataPathResolver.cs b/src/Aspire.Hosting.Browsers/BrowserUserDataPathResolver.cs similarity index 94% rename from src/Aspire.Hosting/BrowserLogs/BrowserUserDataPathResolver.cs rename to src/Aspire.Hosting.Browsers/BrowserUserDataPathResolver.cs index 789ce9bbfec..95c1773ea58 100644 --- a/src/Aspire.Hosting/BrowserLogs/BrowserUserDataPathResolver.cs +++ b/src/Aspire.Hosting.Browsers/BrowserUserDataPathResolver.cs @@ -1,8 +1,10 @@ // 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 ASPIREBROWSERLOGS001 // Type is for evaluation purposes only + using System.Globalization; -using Aspire.Hosting.Resources; +using Aspire.Hosting.Browsers.Resources; namespace Aspire.Hosting; @@ -35,8 +37,8 @@ namespace Aspire.Hosting; // "Profile 1", ...), not the display name the user sees in the browser. Display names live in the user-data-root // "Local State" file under profile.info_cache, keyed by the profile directory name. // -// Both paths are persistent. AppHost shutdown does not delete them and does not close the browser. The next -// AppHost run reads the adoption sidecar and connects to the existing browser via CDP. +// Both paths are persistent. AppHost shutdown does not delete them, but the default pipe-backed browser process is +// still owned by the current AppHost. WebSocket endpoint metadata is only for explicit attach/adoption paths. internal static class BrowserUserDataPathResolver { // SHA-256 prefix length used for the per-AppHost segment. AppHost:PathSha256 is the full hex-encoded @@ -103,7 +105,7 @@ private static string GetAppHostSegment(BrowserConfiguration configuration) throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - MessageStrings.BrowserLogsAppHostPathShaNotAvailable, + BrowserMessageStrings.BrowserLogsAppHostPathShaNotAvailable, BrowserUserDataMode.Isolated)); } diff --git a/src/Aspire.Hosting/BrowserLogs/ChromiumBrowserResolver.cs b/src/Aspire.Hosting.Browsers/ChromiumBrowserResolver.cs similarity index 94% rename from src/Aspire.Hosting/BrowserLogs/ChromiumBrowserResolver.cs rename to src/Aspire.Hosting.Browsers/ChromiumBrowserResolver.cs index 0592d368c58..aca48527a38 100644 --- a/src/Aspire.Hosting/BrowserLogs/ChromiumBrowserResolver.cs +++ b/src/Aspire.Hosting.Browsers/ChromiumBrowserResolver.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using Aspire.Hosting.Browsers.Resources; using System.Text.Json; -using Aspire.Hosting.Resources; namespace Aspire.Hosting; @@ -56,7 +56,7 @@ internal static string ResolveProfileDirectory(string userDataDirectory, string if (!Directory.Exists(userDataDirectory)) { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsUserDataDirectoryNotFound, userDataDirectory)); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsUserDataDirectoryNotFound, userDataDirectory)); } if (TryResolveProfileDirectoryFromDirectoryEntries(userDataDirectory, profile) is { } directMatch) @@ -92,25 +92,25 @@ internal static string ResolveProfileDirectory(string userDataDirectory, string catch (IOException ex) { throw new InvalidOperationException( - string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsUnableToReadProfileMetadata, localStatePath, profile), + string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsUnableToReadProfileMetadata, localStatePath, profile), ex); } catch (UnauthorizedAccessException ex) { throw new InvalidOperationException( - string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsUnableToReadProfileMetadata, localStatePath, profile), + string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsUnableToReadProfileMetadata, localStatePath, profile), ex); } catch (JsonException ex) { throw new InvalidOperationException( - string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsInvalidProfileMetadata, localStatePath, profile), + string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsInvalidProfileMetadata, localStatePath, profile), ex); } } throw new InvalidOperationException( - string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsProfileNotFound, profile, userDataDirectory)); + string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsProfileNotFound, profile, userDataDirectory)); } /// @@ -144,7 +144,7 @@ internal static string ResolveProfileDirectory(string userDataDirectory, string if (match is not null && !string.Equals(match, profileEntry.Name, StringComparison.Ordinal)) { throw new InvalidOperationException( - string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsAmbiguousProfile, profile, userDataDirectory)); + string.Format(CultureInfo.CurrentCulture, BrowserMessageStrings.BrowserLogsAmbiguousProfile, profile, userDataDirectory)); } match = profileEntry.Name; diff --git a/src/Aspire.Hosting/BrowserLogs/ChromiumDevToolsActivePortParser.cs b/src/Aspire.Hosting.Browsers/ChromiumDevToolsActivePortParser.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/ChromiumDevToolsActivePortParser.cs rename to src/Aspire.Hosting.Browsers/ChromiumDevToolsActivePortParser.cs diff --git a/src/Aspire.Hosting/BrowserLogs/IBrowserHost.cs b/src/Aspire.Hosting.Browsers/IBrowserHost.cs similarity index 86% rename from src/Aspire.Hosting/BrowserLogs/IBrowserHost.cs rename to src/Aspire.Hosting.Browsers/IBrowserHost.cs index d1672054559..2c9d296190f 100644 --- a/src/Aspire.Hosting/BrowserLogs/IBrowserHost.cs +++ b/src/Aspire.Hosting.Browsers/IBrowserHost.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 Microsoft.Extensions.Logging; + namespace Aspire.Hosting; // A browser instance/process boundary that one or more tracked log sessions can share. A host either owns the @@ -13,11 +15,10 @@ internal interface IBrowserHost : IAsyncDisposable BrowserHostOwnership Ownership { get; } - // The CDP browser-level WebSocket endpoint. Stable for the lifetime of the host. - Uri DebugEndpoint { get; } + // Browser-level WebSocket endpoint for attach/adoption hosts. Null for pipe-backed owned hosts, where CDP is only + // available through the private host-owned transport. + Uri? DebugEndpoint { get; } - // Null for Adopted hosts (we can't always discover the PID of a browser we didn't spawn) and may also become - // null for Owned hosts after the process has exited. int? ProcessId { get; } // Browser identification surfaced in dashboard properties. e.g. "Microsoft Edge", "Google Chrome". @@ -28,6 +29,13 @@ internal interface IBrowserHost : IAsyncDisposable // termination because sessions can reconnect and reattach to their targets. Task Termination { get; } + // Opens a browser-level CDP connection for a page session. WebSocket-backed hosts create a new connection per + // session; pipe-backed hosts return a shared/multiplexed connection over the private CDP pipe owned by this AppHost. + Task CreateCdpConnectionAsync( + Func eventHandler, + ILogger logger, + CancellationToken cancellationToken); + // Creates a page/tab owned by one tracked browser-log session. The returned session owns only that page target; // disposing it must never close the browser process. Host implementations hide CDP event fanout and recovery // so callers cannot accidentally share a page target or call Browser.close on an adopted browser. @@ -71,10 +79,10 @@ internal enum BrowserPageSessionCompletionKind // releases a shared host, which keeps owned/adopted browser lifetime centralized in BrowserHostRegistry. internal sealed class BrowserHostLease : IAsyncDisposable { - // Lease release acquires the BrowserHostRegistry lock, which is held across CreateHostCoreAsync. Owned-browser - // startup waits up to s_browserEndpointTimeout (30s) for DevToolsActivePort, so the release timeout must exceed - // that worst case to avoid a release-cancellation that strands the registry reference count permanently - // incremented. We also swallow timeouts at the lease boundary so disposal of an owning session never throws. + // Lease release acquires the BrowserHostRegistry lock, which is held across CreateHostCoreAsync. Browser startup can + // be slow, so the release timeout must be long enough to avoid a release-cancellation that strands the registry + // reference count permanently incremented. We also swallow timeouts at the lease boundary so disposal of an owning + // session never throws. private static readonly TimeSpan s_releaseTimeout = TimeSpan.FromSeconds(60); private readonly Func _releaseAsync; @@ -172,7 +180,7 @@ internal enum BrowserHostOwnership // We launched the browser process. Disposing the host kills the process and deletes our endpoint metadata. Owned, - // We connected to a browser someone else launched (typically the user's already-running browser). Disposing - // only closes our CDP connection and any tracked targets we created. The browser keeps running. + // We connected to a browser someone else launched. Disposing only closes our CDP connection and any tracked targets + // we created. The browser keeps running. Adopted, } diff --git a/src/Aspire.Hosting/BrowserLogs/IBrowserLogsSessionManager.cs b/src/Aspire.Hosting.Browsers/IBrowserLogsSessionManager.cs similarity index 100% rename from src/Aspire.Hosting/BrowserLogs/IBrowserLogsSessionManager.cs rename to src/Aspire.Hosting.Browsers/IBrowserLogsSessionManager.cs diff --git a/src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.Designer.cs b/src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.Designer.cs new file mode 100644 index 00000000000..4069395ef78 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.Designer.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Resources; + +#nullable enable + +namespace Aspire.Hosting.Browsers.Resources; + +internal static class BrowserCommandStrings +{ + private static readonly ResourceManager s_resourceManager = new("Aspire.Hosting.Browsers.Resources.BrowserCommandStrings", typeof(BrowserCommandStrings).Assembly); + + internal static CultureInfo? Culture { get; set; } + + internal static string OpenTrackedBrowserDescription => GetString(nameof(OpenTrackedBrowserDescription)); + internal static string OpenTrackedBrowserName => GetString(nameof(OpenTrackedBrowserName)); + internal static string ConfigureTrackedBrowserDescription => GetString(nameof(ConfigureTrackedBrowserDescription)); + internal static string ConfigureTrackedBrowserName => GetString(nameof(ConfigureTrackedBrowserName)); + internal static string ConfigureTrackedBrowserPromptMessage => GetString(nameof(ConfigureTrackedBrowserPromptMessage)); + internal static string ConfigureTrackedBrowserSaveButton => GetString(nameof(ConfigureTrackedBrowserSaveButton)); + internal static string ConfigureTrackedBrowserScopeLabel => GetString(nameof(ConfigureTrackedBrowserScopeLabel)); + internal static string ConfigureTrackedBrowserResourceScopeOption => GetString(nameof(ConfigureTrackedBrowserResourceScopeOption)); + internal static string ConfigureTrackedBrowserGlobalScopeOption => GetString(nameof(ConfigureTrackedBrowserGlobalScopeOption)); + internal static string ConfigureTrackedBrowserGlobalScopeResult => GetString(nameof(ConfigureTrackedBrowserGlobalScopeResult)); + internal static string ConfigureTrackedBrowserBrowserLabel => GetString(nameof(ConfigureTrackedBrowserBrowserLabel)); + internal static string ConfigureTrackedBrowserBrowserDescription => GetString(nameof(ConfigureTrackedBrowserBrowserDescription)); + internal static string ConfigureTrackedBrowserEdgeOption => GetString(nameof(ConfigureTrackedBrowserEdgeOption)); + internal static string ConfigureTrackedBrowserChromeOption => GetString(nameof(ConfigureTrackedBrowserChromeOption)); + internal static string ConfigureTrackedBrowserChromiumOption => GetString(nameof(ConfigureTrackedBrowserChromiumOption)); + internal static string ConfigureTrackedBrowserUserDataModeLabel => GetString(nameof(ConfigureTrackedBrowserUserDataModeLabel)); + internal static string ConfigureTrackedBrowserProfileLabel => GetString(nameof(ConfigureTrackedBrowserProfileLabel)); + internal static string ConfigureTrackedBrowserProfileDescription => GetString(nameof(ConfigureTrackedBrowserProfileDescription)); + internal static string ConfigureTrackedBrowserDefaultProfileOption => GetString(nameof(ConfigureTrackedBrowserDefaultProfileOption)); + internal static string ConfigureTrackedBrowserProfileOptionWithDisplayName => GetString(nameof(ConfigureTrackedBrowserProfileOptionWithDisplayName)); + internal static string ConfigureTrackedBrowserBrowserRequired => GetString(nameof(ConfigureTrackedBrowserBrowserRequired)); + internal static string ConfigureTrackedBrowserUserDataModeRequired => GetString(nameof(ConfigureTrackedBrowserUserDataModeRequired)); + internal static string ConfigureTrackedBrowserProfileRequiresShared => GetString(nameof(ConfigureTrackedBrowserProfileRequiresShared)); + internal static string ConfigureTrackedBrowserSaveToUserSecretsLabel => GetString(nameof(ConfigureTrackedBrowserSaveToUserSecretsLabel)); + internal static string ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured => GetString(nameof(ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured)); + internal static string ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured => GetString(nameof(ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured)); + internal static string ConfigureTrackedBrowserInteractionUnavailable => GetString(nameof(ConfigureTrackedBrowserInteractionUnavailable)); + internal static string ConfigureTrackedBrowserUserSecretsUnavailable => GetString(nameof(ConfigureTrackedBrowserUserSecretsUnavailable)); + internal static string ConfigureTrackedBrowserApplied => GetString(nameof(ConfigureTrackedBrowserApplied)); + internal static string ConfigureTrackedBrowserSaved => GetString(nameof(ConfigureTrackedBrowserSaved)); + internal static string ConfigureTrackedBrowserSaveFailed => GetString(nameof(ConfigureTrackedBrowserSaveFailed)); + internal static string CaptureScreenshotDescription => GetString(nameof(CaptureScreenshotDescription)); + internal static string CaptureScreenshotName => GetString(nameof(CaptureScreenshotName)); + + private static string GetString(string name) => s_resourceManager.GetString(name, Culture)!; +} diff --git a/src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.resx b/src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.resx new file mode 100644 index 00000000000..754970e83eb --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/BrowserCommandStrings.resx @@ -0,0 +1,119 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Open the app in a tracked browser session and stream browser logs to this resource. + + + Open tracked browser + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + Configure tracked browser + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + Apply + + + Scope + + + This resource only ({0}) + {0} is the Aspire resource name + + + All BrowserLogs resources + + + all BrowserLogs resources + + + Browser + + + Choose an installed Chromium-based browser or enter an executable path. + + + Microsoft Edge (msedge) + + + Google Chrome (chrome) + + + Chromium (chromium) + + + User data mode + + + Profile + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + Browser default profile + + + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Browser is required. + + + User data mode must be Shared or Isolated. + + + Profiles can only be selected when user data mode is Shared. + + + Save to AppHost user secrets + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + Capture screenshot + + diff --git a/src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.Designer.cs b/src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.Designer.cs new file mode 100644 index 00000000000..05bb69f6e7e --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.Designer.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Resources; + +#nullable enable + +namespace Aspire.Hosting.Browsers.Resources; + +internal static class BrowserMessageStrings +{ + private static readonly ResourceManager s_resourceManager = new("Aspire.Hosting.Browsers.Resources.BrowserMessageStrings", typeof(BrowserMessageStrings).Assembly); + + internal static CultureInfo? Culture { get; set; } + + internal static string BrowserLogsDefaultProfileName => GetString(nameof(BrowserLogsDefaultProfileName)); + internal static string BrowserLogsEmptyBrowserConfiguration => GetString(nameof(BrowserLogsEmptyBrowserConfiguration)); + internal static string BrowserLogsEmptyProfileConfiguration => GetString(nameof(BrowserLogsEmptyProfileConfiguration)); + internal static string BrowserLogsProfileRequiresSharedUserDataMode => GetString(nameof(BrowserLogsProfileRequiresSharedUserDataMode)); + internal static string BrowserLogsInvalidUserDataModeConfiguration => GetString(nameof(BrowserLogsInvalidUserDataModeConfiguration)); + internal static string BrowserLogsUnableToLocateBrowser => GetString(nameof(BrowserLogsUnableToLocateBrowser)); + internal static string BrowserLogsAppHostPathShaNotAvailable => GetString(nameof(BrowserLogsAppHostPathShaNotAvailable)); + internal static string BrowserLogsUserDataDirectoryNotFound => GetString(nameof(BrowserLogsUserDataDirectoryNotFound)); + internal static string BrowserLogsTrackedBrowserProfileConflict => GetString(nameof(BrowserLogsTrackedBrowserProfileConflict)); + internal static string BrowserLogsUnableToReadProfileMetadata => GetString(nameof(BrowserLogsUnableToReadProfileMetadata)); + internal static string BrowserLogsInvalidProfileMetadata => GetString(nameof(BrowserLogsInvalidProfileMetadata)); + internal static string BrowserLogsProfileNotFound => GetString(nameof(BrowserLogsProfileNotFound)); + internal static string BrowserLogsAmbiguousProfile => GetString(nameof(BrowserLogsAmbiguousProfile)); + internal static string BrowserLogsResourceMissingHttpEndpoint => GetString(nameof(BrowserLogsResourceMissingHttpEndpoint)); + internal static string BrowserLogsEndpointNotAllocated => GetString(nameof(BrowserLogsEndpointNotAllocated)); + + private static string GetString(string name) => s_resourceManager.GetString(name, Culture)!; +} diff --git a/src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.resx b/src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.resx new file mode 100644 index 00000000000..f96360b0569 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/BrowserMessageStrings.resx @@ -0,0 +1,72 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + (default) + + + Tracked browser configuration resolved an empty browser value. + + + Tracked browser configuration resolved an empty profile value. + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.cs.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.cs.xlf new file mode 100644 index 00000000000..bb3692c0d28 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.cs.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.de.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.de.xlf new file mode 100644 index 00000000000..cc3dd21d638 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.de.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.es.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.es.xlf new file mode 100644 index 00000000000..b99882a3824 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.es.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.fr.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.fr.xlf new file mode 100644 index 00000000000..d02e9e0a303 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.fr.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.it.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.it.xlf new file mode 100644 index 00000000000..6fc6eab1c4d --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.it.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ja.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ja.xlf new file mode 100644 index 00000000000..bff29b5c664 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ja.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ko.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ko.xlf new file mode 100644 index 00000000000..c313bf106bf --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ko.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pl.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pl.xlf new file mode 100644 index 00000000000..f44de45f68e --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pl.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pt-BR.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pt-BR.xlf new file mode 100644 index 00000000000..ab64dcbb1af --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.pt-BR.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ru.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ru.xlf new file mode 100644 index 00000000000..51fa5759f00 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.ru.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.tr.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.tr.xlf new file mode 100644 index 00000000000..d6f1920df75 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.tr.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hans.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hans.xlf new file mode 100644 index 00000000000..a428c5c6cdc --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hans.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hant.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hant.xlf new file mode 100644 index 00000000000..95b1a458173 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserCommandStrings.zh-Hant.xlf @@ -0,0 +1,172 @@ + + + + + + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + Capture a screenshot from the active tracked browser session and save it as a PNG artifact. + + + + Capture screenshot + Capture screenshot + + + + Applied tracked browser settings for {0}. + Applied tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Choose an installed Chromium-based browser or enter an executable path. + Choose an installed Chromium-based browser or enter an executable path. + + + + Browser + Browser + + + + Browser is required. + Browser is required. + + + + Google Chrome (chrome) + Google Chrome (chrome) + + + + Chromium (chromium) + Chromium (chromium) + + + + Browser default profile + Browser default profile + + + + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + Choose the browser, profile, user data mode, and scope used by tracked browser sessions. + + + + Microsoft Edge (msedge) + Microsoft Edge (msedge) + + + + All BrowserLogs resources + All BrowserLogs resources + + + + all BrowserLogs resources + all BrowserLogs resources + + + + Tracked browser settings cannot be configured because dashboard interactions are not available. + Tracked browser settings cannot be configured because dashboard interactions are not available. + + + + Configure tracked browser + Configure tracked browser + + + + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. + + + + Profile + Profile + + + + {0} ({1}) + {0} ({1}) + {0} is the Chromium profile directory name, {1} is the Chromium profile display name + + + Profiles can only be selected when user data mode is Shared. + Profiles can only be selected when user data mode is Shared. + + + + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. + + + + This resource only ({0}) + This resource only ({0}) + {0} is the Aspire resource name + + + Apply + Apply + + + + Tracked browser settings could not be saved to user secrets for key '{0}'. + Tracked browser settings could not be saved to user secrets for key '{0}'. + {0} is the user secrets configuration key + + + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. + + + + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. + + + + Save to AppHost user secrets + Save to AppHost user secrets + + + + Saved tracked browser settings for {0}. + Saved tracked browser settings for {0}. + {0} is the resource name or scope that the settings apply to + + + Scope + Scope + + + + User data mode + User data mode + + + + User data mode must be Shared or Isolated. + User data mode must be Shared or Isolated. + + + + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. + + + + Open the app in a tracked browser session and stream browser logs to this resource. + Open the app in a tracked browser session and stream browser logs to this resource. + + + + Open tracked browser + Open tracked browser + + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.cs.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.cs.xlf new file mode 100644 index 00000000000..396592d598c --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.cs.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.de.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.de.xlf new file mode 100644 index 00000000000..d9c5fc6e058 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.de.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.es.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.es.xlf new file mode 100644 index 00000000000..16cd13c66ae --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.es.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.fr.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.fr.xlf new file mode 100644 index 00000000000..1de84eb879c --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.fr.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.it.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.it.xlf new file mode 100644 index 00000000000..487ed8b3c31 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.it.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ja.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ja.xlf new file mode 100644 index 00000000000..c82e00bb947 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ja.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ko.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ko.xlf new file mode 100644 index 00000000000..74011373c69 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ko.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pl.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pl.xlf new file mode 100644 index 00000000000..767b341beb9 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pl.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pt-BR.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pt-BR.xlf new file mode 100644 index 00000000000..7f3c7a7faca --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.pt-BR.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ru.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ru.xlf new file mode 100644 index 00000000000..02389a86b46 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.ru.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.tr.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.tr.xlf new file mode 100644 index 00000000000..a6ed6dfa479 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.tr.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hans.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hans.xlf new file mode 100644 index 00000000000..0e3bf14804e --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hans.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hant.xlf b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hant.xlf new file mode 100644 index 00000000000..52ccbd75fe3 --- /dev/null +++ b/src/Aspire.Hosting.Browsers/Resources/xlf/BrowserMessageStrings.zh-Hant.xlf @@ -0,0 +1,82 @@ + + + + + + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. + {0} is the user data mode that does not require an AppHost path identifier + + + (default) + (default) + + + + Tracked browser configuration resolved an empty browser value. + Tracked browser configuration resolved an empty browser value. + + + + Tracked browser configuration resolved an empty profile value. + Tracked browser configuration resolved an empty profile value. + + + + Endpoint '{0}' for resource '{1}' has not been allocated yet. + Endpoint '{0}' for resource '{1}' has not been allocated yet. + {0} is the endpoint name, {1} is the Aspire resource name + + + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. + {0} is the configured user data mode value, {1} is the user data mode configuration key, {2} and {3} are valid user data mode values + + + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. + {0} is the requested browser profile, {1} is the browser user data directory path + + + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. + {0} is the profile configuration key, {1} is the configured profile value, {2} is the user data mode configuration key, {3} is the configured user data mode, {4} is the required user data mode + + + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. + {0} is the Aspire resource name + + + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. + {0} is the browser user data directory path, {1} is the profile used by the running browser, {2} is the requested profile + + + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. + {0} is the configured browser name or executable path + + + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. + {0} is the Chromium Local State file path, {1} is the requested browser profile + + + Browser user data directory '{0}' was not found. + Browser user data directory '{0}' was not found. + {0} is the browser user data directory path + + + + \ No newline at end of file diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshotExtensions.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshotExtensions.cs new file mode 100644 index 00000000000..6b208d7c628 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshotExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Provides extension methods for creating updated values. +/// +public static class CustomResourceSnapshotExtensions +{ + /// + /// Creates a copy of the resource snapshot with the specified health reports. + /// + /// The resource snapshot to update. + /// The health reports to publish for the resource snapshot. + /// A copy of with updated health reports. + /// Thrown when is . + /// + /// This method is intended for use with + /// and . + /// Updating health reports also recomputes based on the snapshot state. + /// + public static CustomResourceSnapshot WithHealthReports(this CustomResourceSnapshot snapshot, ImmutableArray healthReports) + { + ArgumentNullException.ThrowIfNull(snapshot); + + return snapshot with { HealthReports = healthReports }; + } +} diff --git a/src/Aspire.Hosting/BrowserLogs/BrowserHost.cs b/src/Aspire.Hosting/BrowserLogs/BrowserHost.cs deleted file mode 100644 index aef5ba9cf4d..00000000000 --- a/src/Aspire.Hosting/BrowserLogs/BrowserHost.cs +++ /dev/null @@ -1,420 +0,0 @@ -// 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only - -using System.Globalization; -using System.Text; -using Aspire.Hosting.Dcp.Process; -using Aspire.Hosting.Resources; -using Microsoft.Extensions.Logging; - -namespace Aspire.Hosting; - -// Base implementation for browser hosts. It centralizes the shared mechanics for creating per-page sessions -// while concrete hosts decide who owns the browser process lifetime. -internal abstract class BrowserHost( - BrowserHostIdentity identity, - BrowserHostOwnership ownership, - Uri debugEndpoint, - string browserDisplayName, - ILogger logger, - TimeProvider timeProvider, - bool reuseInitialBlankTarget) : IBrowserHost -{ - private readonly ILogger _logger = logger; - private readonly bool _reuseInitialBlankTarget = reuseInitialBlankTarget; - private readonly TimeProvider _timeProvider = timeProvider; - - public BrowserHostIdentity Identity { get; } = identity; - - public BrowserHostOwnership Ownership { get; } = ownership; - - public Uri DebugEndpoint { get; } = debugEndpoint; - - public abstract int? ProcessId { get; } - - public string BrowserDisplayName { get; } = browserDisplayName; - - public abstract Task Termination { get; } - - public Task CreatePageSessionAsync( - string sessionId, - Uri url, - BrowserConnectionDiagnosticsLogger connectionDiagnostics, - Func eventHandler, - CancellationToken cancellationToken) - { - return CreatePageSessionCoreAsync(sessionId, url, connectionDiagnostics, eventHandler, cancellationToken); - } - - public abstract ValueTask DisposeAsync(); - - private async Task CreatePageSessionCoreAsync( - string sessionId, - Uri url, - BrowserConnectionDiagnosticsLogger connectionDiagnostics, - Func eventHandler, - CancellationToken cancellationToken) - { - return await BrowserPageSession.StartAsync( - this, - sessionId, - url, - connectionDiagnostics, - eventHandler, - _logger, - _timeProvider, - _reuseInitialBlankTarget, - cancellationToken).ConfigureAwait(false); - } -} - -// Host implementation for browsers Aspire starts itself. Owned hosts are responsible for spawning Chromium with a -// browser-level CDP endpoint, writing adoption metadata, and terminating the browser when the final lease is released. -internal sealed class OwnedBrowserHost : BrowserHost -{ - // Browser startup is a local process + file hand-off. Give Chromium enough time to initialize under CI/dev-machine - // load, poll frequently enough for a responsive dashboard command, and cap shutdown so AppHost disposal cannot hang - // forever on a stuck browser process. - // Browser launch races against itself: the OS spawns the process, the process starts up, picks a remote-debugging - // port, and writes DevToolsActivePort. 30 seconds covers cold-start cases (large profile, AV scan, slow disk) while - // still failing fast enough to surface a wedged launch. The 100 ms poll interval is short enough to feel instant - // for warm starts but long enough to avoid burning a core busy-spinning on the file system. - private static readonly TimeSpan s_browserEndpointTimeout = TimeSpan.FromSeconds(30); - private static readonly TimeSpan s_browserEndpointPollInterval = TimeSpan.FromMilliseconds(100); - - private readonly BrowserLogsUserDataDirectory _userDataDirectory; - private readonly IAsyncDisposable _processLifetime; - private readonly Task _processTask; - private readonly Task _termination; - private int _disposed; - - private OwnedBrowserHost( - BrowserHostIdentity identity, - Uri debugEndpoint, - string browserDisplayName, - int processId, - BrowserLogsUserDataDirectory userDataDirectory, - IAsyncDisposable processLifetime, - Task processTask, - ILogger logger, - TimeProvider timeProvider) - : base(identity, BrowserHostOwnership.Owned, debugEndpoint, browserDisplayName, logger, timeProvider, reuseInitialBlankTarget: true) - { - _processLifetime = processLifetime; - _processTask = processTask; - _termination = processTask; - _userDataDirectory = userDataDirectory; - ProcessId = processId; - } - - public override int? ProcessId { get; } - - public override Task Termination => _termination; - - private static string BuildBrowserArguments(BrowserLogsUserDataDirectory userDataDirectory) - { - // Chromium writes DevToolsActivePort only when remote debugging is enabled. Let it choose the port so - // playground runs do not collide with a user's existing browser or another AppHost. The initial about:blank - // page gives owned hosts a predictable first page target that can be navigated instead of leaving an extra - // blank tab. - List arguments = - [ - $"--user-data-dir={userDataDirectory.Path}", - "--remote-debugging-address=127.0.0.1", - "--remote-debugging-port=0", - "--no-first-run", - "--no-default-browser-check", - "--new-window", - "--allow-insecure-localhost" - ]; - - if (userDataDirectory.ProfileDirectoryName is { } profileDirectoryName) - { - arguments.Add($"--profile-directory={profileDirectoryName}"); - } - - arguments.Add("about:blank"); - - return BuildCommandLine(arguments); - } - - private static string BuildCommandLine(IReadOnlyList arguments) - { - var builder = new StringBuilder(); - - for (var i = 0; i < arguments.Count; i++) - { - if (i > 0) - { - builder.Append(' '); - } - - AppendCommandLineArgument(builder, arguments[i]); - } - - return builder.ToString(); - } - - // Adapted from dotnet/runtime PasteArguments.AppendArgument so ProcessSpec can safely represent Chromium flags. - private static void AppendCommandLineArgument(StringBuilder builder, string argument) - { - if (argument.Length != 0 && !argument.AsSpan().ContainsAny(' ', '\t', '"')) - { - builder.Append(argument); - return; - } - - builder.Append('"'); - - var index = 0; - while (index < argument.Length) - { - var character = argument[index++]; - if (character == '\\') - { - var backslashCount = 1; - while (index < argument.Length && argument[index] == '\\') - { - index++; - backslashCount++; - } - - if (index == argument.Length) - { - builder.Append('\\', backslashCount * 2); - } - else if (argument[index] == '"') - { - builder.Append('\\', backslashCount * 2 + 1); - builder.Append('"'); - index++; - } - else - { - builder.Append('\\', backslashCount); - } - - continue; - } - - if (character == '"') - { - builder.Append('\\'); - builder.Append('"'); - continue; - } - - builder.Append(character); - } - - builder.Append('"'); - } - - public static async Task StartAsync( - BrowserHostIdentity identity, - string browserDisplayName, - BrowserLogsUserDataDirectory userDataDirectory, - ILogger logger, - TimeProvider timeProvider, - CancellationToken cancellationToken) - { - var processStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var devToolsActivePortFilePath = Path.Combine(userDataDirectory.Path, "DevToolsActivePort"); - // DevToolsActivePort is Chromium's hand-off file for the browser-level websocket. Real profile directories can - // contain a stale file from a previous run, and a live browser can keep it locked, so remember the previous - // timestamp and only accept a fresh write from the process we just launched. - var previousWriteTimeUtc = PrepareBrowserEndpointFile(devToolsActivePortFilePath, logger); - // Clear Aspire's adoption sidecar before launch so a later AcquireAsync cannot adopt stale metadata while this - // owned process is still proving which endpoint Chromium actually opened. - BrowserEndpointDiscovery.DeleteEndpointMetadata(userDataDirectory.Path); - - var processSpec = new ProcessSpec(identity.ExecutablePath) - { - Arguments = BuildBrowserArguments(userDataDirectory), - InheritEnv = true, - OnErrorData = error => logger.LogTrace("Tracked browser stderr: {Line}", error), - OnOutputData = output => logger.LogTrace("Tracked browser stdout: {Line}", output), - OnStart = processId => processStarted.TrySetResult(processId), - ThrowOnNonZeroReturnCode = false - }; - - var (processTask, processLifetime) = ProcessUtil.Run(processSpec); - int processId; - Uri browserEndpoint; - try - { - processId = await WaitForProcessStartAsync(processStarted.Task, processTask, cancellationToken).ConfigureAwait(false); - browserEndpoint = await WaitForBrowserEndpointAsync(processTask, devToolsActivePortFilePath, previousWriteTimeUtc, logger, timeProvider, cancellationToken).ConfigureAwait(false); - // Once Chromium has written DevToolsActivePort and responded with a browser endpoint, write our sidecar so a - // later AppHost run can adopt the same debug-enabled browser instead of opening a second window. - await BrowserEndpointDiscovery.WriteAsync(identity, userDataDirectory.ProfileDirectoryName, browserEndpoint, processId, cancellationToken).ConfigureAwait(false); - } - catch - { - await processLifetime.DisposeAsync().ConfigureAwait(false); - userDataDirectory.Dispose(); - throw; - } - - return new OwnedBrowserHost( - identity, - browserEndpoint, - browserDisplayName, - processId, - userDataDirectory, - processLifetime, - processTask, - logger, - timeProvider); - } - - public override async ValueTask DisposeAsync() - { - if (Interlocked.Exchange(ref _disposed, 1) != 0) - { - return; - } - - // Both Shared and Isolated point at a persistent Aspire-managed user data directory. AppHost shutdown does - // not close the browser, does not delete the adoption sidecar, and does not delete the user data directory. - // The next AppHost run reads the sidecar and connects to this browser via CDP. The user closes the browser - // when they are done with it. - // - // We deliberately do not dispose _processLifetime, which would terminate the browser process. The Process - // handle leaks until the AppHost exits; ProcessDisposable has no finalizer that would kill the process on - // GC, so the browser keeps running. - _ = _processLifetime; - _ = _processTask; - _ = _userDataDirectory; - await Task.CompletedTask.ConfigureAwait(false); - } - - private static async Task WaitForProcessStartAsync(Task processStarted, Task processTask, CancellationToken cancellationToken) - { - var completedTask = await Task.WhenAny(processStarted, processTask).WaitAsync(cancellationToken).ConfigureAwait(false); - if (completedTask == processStarted) - { - return await processStarted.ConfigureAwait(false); - } - - var result = await processTask.ConfigureAwait(false); - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsProcessExitedBeforeProcessId, result.ExitCode)); - } - - private static async Task WaitForBrowserEndpointAsync( - Task processTask, - string devToolsActivePortFilePath, - DateTime? previousWriteTimeUtc, - ILogger logger, - TimeProvider timeProvider, - CancellationToken cancellationToken) - { - var timeoutAt = timeProvider.GetUtcNow() + s_browserEndpointTimeout; - logger.LogTrace("Waiting up to {Timeout} for tracked browser to publish DevToolsActivePort at '{DevToolsActivePortFilePath}'.", s_browserEndpointTimeout, devToolsActivePortFilePath); - - while (timeProvider.GetUtcNow() < timeoutAt) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (processTask.IsCompleted) - { - var result = await processTask.ConfigureAwait(false); - throw new InvalidOperationException( - string.Format(CultureInfo.CurrentCulture, MessageStrings.BrowserLogsProcessExitedBeforeDebugEndpoint, result.ExitCode, devToolsActivePortFilePath)); - } - - try - { - if (File.Exists(devToolsActivePortFilePath)) - { - if (previousWriteTimeUtc is { } previousWriteTime && - File.GetLastWriteTimeUtc(devToolsActivePortFilePath) <= previousWriteTime) - { - logger.LogTrace("Ignoring stale tracked browser endpoint metadata '{DevToolsActivePortFilePath}' while waiting for a fresh Chromium write.", devToolsActivePortFilePath); - await Task.Delay(s_browserEndpointPollInterval, cancellationToken).ConfigureAwait(false); - continue; - } - - var contents = await File.ReadAllTextAsync(devToolsActivePortFilePath, cancellationToken).ConfigureAwait(false); - if (ChromiumDevToolsActivePortParser.TryParseBrowserDebugEndpoint(contents) is { } browserEndpoint) - { - logger.LogTrace("Read tracked browser debug endpoint '{BrowserDebugEndpoint}' from '{DevToolsActivePortFilePath}'.", browserEndpoint, devToolsActivePortFilePath); - return browserEndpoint; - } - - logger.LogTrace("Tracked browser endpoint metadata '{DevToolsActivePortFilePath}' was present but not parseable yet.", devToolsActivePortFilePath); - } - } - catch (IOException ex) - { - logger.LogTrace(ex, "Unable to read tracked browser endpoint metadata '{DevToolsActivePortFilePath}' yet.", devToolsActivePortFilePath); - } - catch (UnauthorizedAccessException ex) - { - logger.LogTrace(ex, "Unable to read tracked browser endpoint metadata '{DevToolsActivePortFilePath}' yet.", devToolsActivePortFilePath); - } - - await Task.Delay(s_browserEndpointPollInterval, cancellationToken).ConfigureAwait(false); - } - - throw new TimeoutException($"Timed out waiting for the tracked browser to write '{devToolsActivePortFilePath}'."); - } - - private static DateTime? PrepareBrowserEndpointFile(string devToolsActivePortFilePath, ILogger logger) - { - if (!File.Exists(devToolsActivePortFilePath)) - { - return null; - } - - var previousWriteTimeUtc = File.GetLastWriteTimeUtc(devToolsActivePortFilePath); - - try - { - File.Delete(devToolsActivePortFilePath); - return null; - } - catch (IOException ex) - { - logger.LogDebug(ex, "Unable to delete stale tracked browser endpoint metadata '{DevToolsActivePortFilePath}'. Waiting for a fresh file instead.", devToolsActivePortFilePath); - return previousWriteTimeUtc; - } - catch (UnauthorizedAccessException ex) - { - logger.LogDebug(ex, "Unable to delete stale tracked browser endpoint metadata '{DevToolsActivePortFilePath}'. Waiting for a fresh file instead.", devToolsActivePortFilePath); - return previousWriteTimeUtc; - } - } -} - -// Host implementation for browsers Aspire discovers from validated endpoint metadata. Adopted hosts create and close -// tracked targets, but never terminate the browser process because it may be the user's normal browser. -internal sealed class AdoptedBrowserHost : BrowserHost -{ - private readonly TaskCompletionSource _terminationSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - - // An adopted browser may already contain user-owned tabs. Always create a new target for Aspire rather than reusing - // an arbitrary about:blank page that happened to exist in the user's real browser. - public AdoptedBrowserHost( - BrowserHostIdentity identity, - Uri debugEndpoint, - string browserDisplayName, - ILogger logger, - TimeProvider timeProvider) - : base(identity, BrowserHostOwnership.Adopted, debugEndpoint, browserDisplayName, logger, timeProvider, reuseInitialBlankTarget: false) - { - } - - public override int? ProcessId => null; - - public override Task Termination => _terminationSource.Task; - - public override ValueTask DisposeAsync() - { - _terminationSource.TrySetResult(); - - return ValueTask.CompletedTask; - } -} diff --git a/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs b/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs index a62764b3649..6135bb20f2a 100644 --- a/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs +++ b/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs @@ -105,303 +105,6 @@ internal static string DeleteParameterName { } } - /// - /// Looks up a localized string similar to Open the app in a tracked browser session and stream browser logs to this resource.. - /// - internal static string OpenTrackedBrowserDescription { - get { - return ResourceManager.GetString("OpenTrackedBrowserDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open tracked browser. - /// - internal static string OpenTrackedBrowserName { - get { - return ResourceManager.GetString("OpenTrackedBrowserName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Choose the browser, profile, and scope used by tracked browser sessions.. - /// - internal static string ConfigureTrackedBrowserDescription { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configure tracked browser. - /// - internal static string ConfigureTrackedBrowserName { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings.. - /// - internal static string ConfigureTrackedBrowserPromptMessage { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserPromptMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Apply. - /// - internal static string ConfigureTrackedBrowserSaveButton { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserSaveButton", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scope. - /// - internal static string ConfigureTrackedBrowserScopeLabel { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserScopeLabel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This resource only ({0}). - /// - internal static string ConfigureTrackedBrowserResourceScopeOption { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserResourceScopeOption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All BrowserLogs resources. - /// - internal static string ConfigureTrackedBrowserGlobalScopeOption { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserGlobalScopeOption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to all BrowserLogs resources. - /// - internal static string ConfigureTrackedBrowserGlobalScopeResult { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserGlobalScopeResult", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Browser. - /// - internal static string ConfigureTrackedBrowserBrowserLabel { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserBrowserLabel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Choose an installed Chromium-based browser or enter an executable path.. - /// - internal static string ConfigureTrackedBrowserBrowserDescription { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserBrowserDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Microsoft Edge (msedge). - /// - internal static string ConfigureTrackedBrowserEdgeOption { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserEdgeOption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Google Chrome (chrome). - /// - internal static string ConfigureTrackedBrowserChromeOption { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserChromeOption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Chromium (chromium). - /// - internal static string ConfigureTrackedBrowserChromiumOption { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserChromiumOption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User data mode. - /// - internal static string ConfigureTrackedBrowserUserDataModeLabel { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserUserDataModeLabel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Profile. - /// - internal static string ConfigureTrackedBrowserProfileLabel { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserProfileLabel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name.. - /// - internal static string ConfigureTrackedBrowserProfileDescription { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserProfileDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Browser default profile. - /// - internal static string ConfigureTrackedBrowserDefaultProfileOption { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserDefaultProfileOption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} ({1}). - /// - internal static string ConfigureTrackedBrowserProfileOptionWithDisplayName { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserProfileOptionWithDisplayName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Browser is required.. - /// - internal static string ConfigureTrackedBrowserBrowserRequired { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserBrowserRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User data mode must be Shared or Isolated.. - /// - internal static string ConfigureTrackedBrowserUserDataModeRequired { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserUserDataModeRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Profiles can only be selected when user data mode is Shared.. - /// - internal static string ConfigureTrackedBrowserProfileRequiresShared { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserProfileRequiresShared", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Save to AppHost user secrets. - /// - internal static string ConfigureTrackedBrowserSaveToUserSecretsLabel { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserSaveToUserSecretsLabel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them.. - /// - internal static string ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User secrets are not configured for this AppHost, so settings can only apply to this running AppHost.. - /// - internal static string ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser settings cannot be configured because dashboard interactions are not available.. - /// - internal static string ConfigureTrackedBrowserInteractionUnavailable { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserInteractionUnavailable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser settings cannot be saved because the AppHost does not have user secrets configured.. - /// - internal static string ConfigureTrackedBrowserUserSecretsUnavailable { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserUserSecretsUnavailable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Applied tracked browser settings for {0}.. - /// - internal static string ConfigureTrackedBrowserApplied { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserApplied", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Saved tracked browser settings for {0}.. - /// - internal static string ConfigureTrackedBrowserSaved { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserSaved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser settings could not be saved to user secrets for key '{0}'.. - /// - internal static string ConfigureTrackedBrowserSaveFailed { - get { - return ResourceManager.GetString("ConfigureTrackedBrowserSaveFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Capture a screenshot from the active tracked browser session and save it as a PNG artifact.. - /// - internal static string CaptureScreenshotDescription { - get { - return ResourceManager.GetString("CaptureScreenshotDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Capture screenshot. - /// - internal static string CaptureScreenshotName { - get { - return ResourceManager.GetString("CaptureScreenshotName", resourceCulture); - } - } - /// /// Looks up a localized string similar to Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes.. /// diff --git a/src/Aspire.Hosting/Resources/CommandStrings.resx b/src/Aspire.Hosting/Resources/CommandStrings.resx index 8d39791445a..c955543bf60 100644 --- a/src/Aspire.Hosting/Resources/CommandStrings.resx +++ b/src/Aspire.Hosting/Resources/CommandStrings.resx @@ -150,110 +150,6 @@ Delete parameter - - Open the app in a tracked browser session and stream browser logs to this resource. - - - Open tracked browser - - - Choose the browser, profile, and scope used by tracked browser sessions. - - - Configure tracked browser - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - Apply - - - Scope - - - This resource only ({0}) - {0} is the parent resource name. - - - All BrowserLogs resources - - - all BrowserLogs resources - - - Browser - - - Choose an installed Chromium-based browser or enter an executable path. - - - Microsoft Edge (msedge) - - - Google Chrome (chrome) - - - Chromium (chromium) - - - User data mode - - - Profile - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - Browser default profile - - - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Browser is required. - - - User data mode must be Shared or Isolated. - - - Profiles can only be selected when user data mode is Shared. - - - Save to AppHost user secrets - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - Capture screenshot - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/MessageStrings.Designer.cs b/src/Aspire.Hosting/Resources/MessageStrings.Designer.cs index 5522a8cee36..452465fe7fc 100644 --- a/src/Aspire.Hosting/Resources/MessageStrings.Designer.cs +++ b/src/Aspire.Hosting/Resources/MessageStrings.Designer.cs @@ -177,157 +177,5 @@ internal static string ResourceMayFailToStart { } } - /// - /// Looks up a localized string similar to (default). - /// - internal static string BrowserLogsDefaultProfileName { - get { - return ResourceManager.GetString("BrowserLogsDefaultProfileName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser configuration resolved an empty browser value.. - /// - internal static string BrowserLogsEmptyBrowserConfiguration { - get { - return ResourceManager.GetString("BrowserLogsEmptyBrowserConfiguration", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser configuration resolved an empty profile value.. - /// - internal static string BrowserLogsEmptyProfileConfiguration { - get { - return ResourceManager.GetString("BrowserLogsEmptyProfileConfiguration", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'.. - /// - internal static string BrowserLogsProfileRequiresSharedUserDataMode { - get { - return ResourceManager.GetString("BrowserLogsProfileRequiresSharedUserDataMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'.. - /// - internal static string BrowserLogsInvalidUserDataModeConfiguration { - get { - return ResourceManager.GetString("BrowserLogsInvalidUserDataModeConfiguration", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path.. - /// - internal static string BrowserLogsUnableToLocateBrowser { - get { - return ResourceManager.GetString("BrowserLogsUnableToLocateBrowser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost.. - /// - internal static string BrowserLogsAppHostPathShaNotAvailable { - get { - return ResourceManager.GetString("BrowserLogsAppHostPathShaNotAvailable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Browser user data directory '{0}' was not found.. - /// - internal static string BrowserLogsUserDataDirectoryNotFound { - get { - return ResourceManager.GetString("BrowserLogsUserDataDirectoryNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode.. - /// - internal static string BrowserLogsTrackedBrowserProfileConflict { - get { - return ResourceManager.GetString("BrowserLogsTrackedBrowserProfileConflict", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'.. - /// - internal static string BrowserLogsUnableToReadProfileMetadata { - get { - return ResourceManager.GetString("BrowserLogsUnableToReadProfileMetadata", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'.. - /// - internal static string BrowserLogsInvalidProfileMetadata { - get { - return ResourceManager.GetString("BrowserLogsInvalidProfileMetadata", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata.. - /// - internal static string BrowserLogsProfileNotFound { - get { - return ResourceManager.GetString("BrowserLogsProfileNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead.. - /// - internal static string BrowserLogsAmbiguousProfile { - get { - return ResourceManager.GetString("BrowserLogsAmbiguousProfile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to.. - /// - internal static string BrowserLogsResourceMissingHttpEndpoint { - get { - return ResourceManager.GetString("BrowserLogsResourceMissingHttpEndpoint", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Endpoint '{0}' for resource '{1}' has not been allocated yet.. - /// - internal static string BrowserLogsEndpointNotAllocated { - get { - return ResourceManager.GetString("BrowserLogsEndpointNotAllocated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser process exited with code {0} before reporting its process id.. - /// - internal static string BrowserLogsProcessExitedBeforeProcessId { - get { - return ResourceManager.GetString("BrowserLogsProcessExitedBeforeProcessId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'.. - /// - internal static string BrowserLogsProcessExitedBeforeDebugEndpoint { - get { - return ResourceManager.GetString("BrowserLogsProcessExitedBeforeDebugEndpoint", resourceCulture); - } - } } } diff --git a/src/Aspire.Hosting/Resources/MessageStrings.resx b/src/Aspire.Hosting/Resources/MessageStrings.resx index 5a6c227fc60..bd1bc4af64c 100644 --- a/src/Aspire.Hosting/Resources/MessageStrings.resx +++ b/src/Aspire.Hosting/Resources/MessageStrings.resx @@ -156,69 +156,4 @@ Resource '{0}' may fail to start: {1} - - (default) - - - Tracked browser configuration resolved an empty browser value. - - - Tracked browser configuration resolved an empty profile value. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - Browser user data directory '{0}' was not found. - {0} is the user data directory. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf index 3389a9a29c8..ebde0302553 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Odstranit hodnotu parametru @@ -167,16 +12,6 @@ Odstranit parametr - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf index 24fe8e1466d..adbf6bbe364 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Parameterwert löschen @@ -167,16 +12,6 @@ Parameter löschen - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf index 8f483a3d46d..d2f67ba89e0 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Eliminar valor de parámetro @@ -167,16 +12,6 @@ Eliminar parámetro - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf index 3a85e4c09ea..f69fe25f71a 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Supprimer la valeur du paramètre @@ -167,16 +12,6 @@ Supprimer le paramètre - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf index 5ff49e39319..783c0e16512 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Elimina valore parametro @@ -167,16 +12,6 @@ Elimina parametro - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf index 79ccb85e9ad..4a3f1fa42d8 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value パラメーター値の削除 @@ -167,16 +12,6 @@ パラメーターの削除 - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf index b1a6ada9337..16ac639329f 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value 매개 변수 값 삭제 @@ -167,16 +12,6 @@ 매개 변수 삭제 - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf index d71a4e6392c..f959a404a13 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Usuń wartość parametru @@ -167,16 +12,6 @@ Usuń parametr - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf index 7cc51e667d1..c89ebb00271 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Excluir valor do parâmetro @@ -167,16 +12,6 @@ Excluir parâmetro - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf index bfc119e3e37..e590c123ad5 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Удалить значение параметра @@ -167,16 +12,6 @@ Удалить параметр - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf index 86094d777a6..fb14686e425 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value Parametre değerini sil @@ -167,16 +12,6 @@ Parametreyi sil - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf index dc21d428531..430e2a03c4d 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value 删除参数值 @@ -167,16 +12,6 @@ 删除参数 - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf index 1cff903f5bc..5b5ef0297a8 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf @@ -2,161 +2,6 @@ - - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - Capture a screenshot from the active tracked browser session and save it as a PNG artifact. - - - - Capture screenshot - Capture screenshot - - - - Applied tracked browser settings for {0}. - Applied tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Choose an installed Chromium-based browser or enter an executable path. - Choose an installed Chromium-based browser or enter an executable path. - - - - Browser - Browser - - - - Browser is required. - Browser is required. - - - - Google Chrome (chrome) - Google Chrome (chrome) - - - - Chromium (chromium) - Chromium (chromium) - - - - Browser default profile - Browser default profile - - - - Choose the browser, profile, and scope used by tracked browser sessions. - Choose the browser, profile, and scope used by tracked browser sessions. - - - - Microsoft Edge (msedge) - Microsoft Edge (msedge) - - - - All BrowserLogs resources - All BrowserLogs resources - - - - all BrowserLogs resources - all BrowserLogs resources - - - - Tracked browser settings cannot be configured because dashboard interactions are not available. - Tracked browser settings cannot be configured because dashboard interactions are not available. - - - - Configure tracked browser - Configure tracked browser - - - - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - Choose a Chromium profile from the Aspire-managed browser hive, or enter a profile directory/name. - - - - Profile - Profile - - - - {0} ({1}) - {0} ({1}) - {0} is the Chromium profile directory name, {1} is the profile display name. - - - Profiles can only be selected when user data mode is Shared. - Profiles can only be selected when user data mode is Shared. - - - - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - Choose tracked browser settings. Resource-specific settings override global BrowserLogs settings. - - - - This resource only ({0}) - This resource only ({0}) - {0} is the parent resource name. - - - Apply - Apply - - - - Tracked browser settings could not be saved to user secrets for key '{0}'. - Tracked browser settings could not be saved to user secrets for key '{0}'. - {0} is the user secrets key. - - - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - Save these settings to [user secrets](https://aka.ms/aspire/user-secrets) so future AppHost runs use them. - - - - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - User secrets are not configured for this AppHost, so settings can only apply to this running AppHost. - - - - Save to AppHost user secrets - Save to AppHost user secrets - - - - Saved tracked browser settings for {0}. - Saved tracked browser settings for {0}. - {0} is the selected configuration scope. - - - Scope - Scope - - - - User data mode - User data mode - - - - User data mode must be Shared or Isolated. - User data mode must be Shared or Isolated. - - - - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - Tracked browser settings cannot be saved because the AppHost does not have user secrets configured. - - Delete parameter value 刪除參數值 @@ -167,16 +12,6 @@ 刪除參數 - - Open the app in a tracked browser session and stream browser logs to this resource. - Open the app in a tracked browser session and stream browser logs to this resource. - - - - Open tracked browser - Open tracked browser - - Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. Stop the resource, rebuild the project from source, and restart. Use this to apply source code changes. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.cs.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.cs.xlf index 31cab53b700..543d1a3979f 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.cs.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.cs.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Anonymní svazky nemůžou být jen pro čtení. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.de.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.de.xlf index 5f06d2085fd..9b298673e5a 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.de.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.de.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Anonyme Volumes können nicht schreibgeschützt sein. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.es.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.es.xlf index 35b24a70c4f..f458d0e0774 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.es.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.es.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Los volúmenes anónimos no pueden ser de solo lectura. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.fr.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.fr.xlf index e55c1cfbadf..ee369e66477 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.fr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.fr.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Les volumes anonymes ne peuvent pas être en lecture seule. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.it.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.it.xlf index ff58500f9b3..c13f0da17d1 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.it.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.it.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. I volumi anonimi non possono essere di sola lettura. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.ja.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.ja.xlf index 0aa3bd991ba..b268c42a2b4 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.ja.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.ja.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. 匿名ボリュームを読み取り専用にすることはできません。 diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.ko.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.ko.xlf index d0345716ec2..d5947778ca9 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.ko.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.ko.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. 익명 볼륨은 읽기 전용일 수 없습니다. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.pl.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.pl.xlf index c3999377582..edbe59b5248 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.pl.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.pl.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Woluminy anonimowe nie mogą być tylko do odczytu. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.pt-BR.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.pt-BR.xlf index a7679897c1d..4bd628aab1c 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.pt-BR.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.pt-BR.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Volumes anônimos não podem ser somente leitura. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.ru.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.ru.xlf index d6228548040..f976374fb51 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.ru.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.ru.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Анонимные тома не могут быть доступны только для чтения. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.tr.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.tr.xlf index 9e763e3fbe2..432ed7ef707 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.tr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.tr.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. Anonim birimler salt okunur olamaz. diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hans.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hans.xlf index 60aa23ee792..d83606fb8db 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hans.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hans.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. 匿名卷不能为只读。 diff --git a/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hant.xlf b/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hant.xlf index acaab71eda1..2358ea63c17 100644 --- a/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hant.xlf +++ b/src/Aspire.Hosting/Resources/xlf/MessageStrings.zh-Hant.xlf @@ -2,91 +2,6 @@ - - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - Browser profile '{0}' matched multiple Chromium profiles under '{1}'. Specify the profile directory name instead. - {0} is the requested profile, {1} is the user data directory. - - - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - Cannot resolve the isolated browser user data directory because the AppHost path identifier is not available. Use '{0}' user data mode or run from a configured AppHost. - {0} is the Isolated user data mode value. - - - (default) - (default) - - - - Tracked browser configuration resolved an empty browser value. - Tracked browser configuration resolved an empty browser value. - - - - Tracked browser configuration resolved an empty profile value. - Tracked browser configuration resolved an empty profile value. - - - - Endpoint '{0}' for resource '{1}' has not been allocated yet. - Endpoint '{0}' for resource '{1}' has not been allocated yet. - {0} is the endpoint name, {1} is the resource name. - - - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - Chromium profile metadata in '{0}' is invalid while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - Tracked browser configuration value '{0}' is not a valid '{1}'. Expected '{2}' or '{3}'. - {0} is the configured value, {1} is the user data mode configuration key, {2} and {3} are valid enum values. - - - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - Tracked browser process exited with code {0} before the debug endpoint metadata was written to '{1}'. - {0} is the browser process exit code, {1} is the DevToolsActivePort file path. - - - Tracked browser process exited with code {0} before reporting its process id. - Tracked browser process exited with code {0} before reporting its process id. - {0} is the browser process exit code. - - - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - Browser profile '{0}' was not found under '{1}'. Specify the profile directory name (for example 'Default' or 'Profile 1') or a browser profile name from Chromium's profile metadata. - {0} is the requested profile, {1} is the user data directory. - - - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - Tracked browser configuration set '{0}' to '{1}' while '{2}' is '{3}'. Profiles can only be selected when '{2}' is '{4}'. - {0} is the profile configuration key, {1} is the configured profile, {2} is the user data mode configuration key, {3} is Isolated, {4} is Shared. - - - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - Resource '{0}' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to. - {0} is the resource name. - - - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - A tracked browser is already running for user data directory '{0}' with profile '{1}'. The requested profile is '{2}'. Close the existing tracked browser session or use isolated user data mode. - {0} is the user data directory, {1} is the existing profile, {2} is the requested profile. - - - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - Unable to locate browser '{0}'. Specify an installed Chromium-based browser or an explicit executable path. - {0} is the requested browser name or path. - - - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - Unable to read Chromium profile metadata from '{0}' while resolving browser profile '{1}'. - {0} is the Local State file path, {1} is the requested profile. - - - Browser user data directory '{0}' was not found. - Browser user data directory '{0}' was not found. - {0} is the user data directory. - Anonymous volumes cannot be read-only. 匿名磁碟區不可為唯讀。 diff --git a/tests/Aspire.Hosting.Browsers.Tests/Aspire.Hosting.Browsers.Tests.csproj b/tests/Aspire.Hosting.Browsers.Tests/Aspire.Hosting.Browsers.Tests.csproj new file mode 100644 index 00000000000..17036ad07e5 --- /dev/null +++ b/tests/Aspire.Hosting.Browsers.Tests/Aspire.Hosting.Browsers.Tests.csproj @@ -0,0 +1,43 @@ + + + + $(DefaultTargetFramework) + + false + false + 8-core-ubuntu-latest + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Aspire.Hosting.Tests/BrowserConnectionDiagnosticsLoggerTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserConnectionDiagnosticsLoggerTests.cs similarity index 98% rename from tests/Aspire.Hosting.Tests/BrowserConnectionDiagnosticsLoggerTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/BrowserConnectionDiagnosticsLoggerTests.cs index d935ca18d2d..4a58d26bd6c 100644 --- a/tests/Aspire.Hosting.Tests/BrowserConnectionDiagnosticsLoggerTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserConnectionDiagnosticsLoggerTests.cs @@ -4,7 +4,7 @@ using System.Net.WebSockets; using Aspire.Hosting.Tests.Utils; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class BrowserConnectionDiagnosticsLoggerTests diff --git a/tests/Aspire.Hosting.Browsers.Tests/BrowserHostTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserHostTests.cs new file mode 100644 index 00000000000..f6f6bc2e415 --- /dev/null +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserHostTests.cs @@ -0,0 +1,166 @@ +// 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only + +using System.IO.Pipelines; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Aspire.Hosting.Browsers.Tests; + +[Trait("Partition", "2")] +public class BrowserHostTests +{ + [Fact] + public async Task OwnedBrowserHost_StartAsync_UsesPipeTransportAndDeletesEndpointMetadata() + { + var userDataDirectory = Directory.CreateTempSubdirectory(); + try + { + var browserExecutable = Path.Combine(userDataDirectory.FullName, "browser"); + await File.WriteAllTextAsync(browserExecutable, string.Empty); + var devToolsActivePortPath = Path.Combine(userDataDirectory.FullName, "DevToolsActivePort"); + await File.WriteAllTextAsync(devToolsActivePortPath, "12345\n/devtools/browser/stale"); + + var identity = new BrowserHostIdentity(browserExecutable, userDataDirectory.FullName); + await BrowserEndpointDiscovery.WriteAsync( + identity, + profileDirectoryName: "Profile 1", + new Uri("ws://127.0.0.1:9/devtools/browser/stale"), + Environment.ProcessId, + CancellationToken.None); + + FakePipeBrowserProcess? fakeProcess = null; + IReadOnlyList? capturedArguments = null; + var host = await OwnedBrowserHost.StartAsync( + identity, + browserDisplayName: "Test Browser", + BrowserLogsUserDataDirectory.CreatePersistent(userDataDirectory.FullName, profileDirectoryName: "Profile 1"), + NullLogger.Instance, + TimeProvider.System, + CancellationToken.None, + startPipeBrowserProcess: (executablePath, arguments) => + { + Assert.Equal(browserExecutable, executablePath); + capturedArguments = [.. arguments]; + fakeProcess = new FakePipeBrowserProcess(); + return fakeProcess; + }); + + try + { + Assert.Null(host.DebugEndpoint); + Assert.Equal(4242, host.ProcessId); + Assert.False(File.Exists(devToolsActivePortPath)); + Assert.False(File.Exists(BrowserEndpointDiscovery.GetEndpointMetadataFilePath(userDataDirectory.FullName))); + + Assert.NotNull(capturedArguments); + Assert.Contains($"--user-data-dir={userDataDirectory.FullName}", capturedArguments); + Assert.Contains("--profile-directory=Profile 1", capturedArguments); + Assert.Contains("about:blank", capturedArguments); + Assert.DoesNotContain(capturedArguments, static argument => argument.StartsWith("--remote-debugging-address=", StringComparison.Ordinal)); + Assert.DoesNotContain(capturedArguments, static argument => argument.StartsWith("--remote-debugging-port=", StringComparison.Ordinal)); + + await using var connection = await host.CreateCdpConnectionAsync( + static _ => ValueTask.CompletedTask, + NullLogger.Instance, + CancellationToken.None); + + var enableDiscoveryTask = connection.EnableTargetDiscoveryAsync(CancellationToken.None); + using var command = JsonDocument.Parse(await fakeProcess!.ReadFrameAsync().DefaultTimeout()); + Assert.Equal(BrowserLogsCdpProtocol.TargetSetDiscoverTargetsMethod, command.RootElement.GetProperty("method").GetString()); + + var responseFrame = "{\"id\":" + command.RootElement.GetProperty("id").GetInt64() + ",\"result\":{}}"; + await fakeProcess.SendFrameAsync(responseFrame).DefaultTimeout(); + await enableDiscoveryTask.DefaultTimeout(); + } + finally + { + await host.DisposeAsync(); + } + + Assert.True(fakeProcess?.Disposed is true); + } + finally + { + userDataDirectory.Delete(recursive: true); + } + } + + private sealed class FakePipeBrowserProcess : IBrowserLogsPipeBrowserProcess + { + private readonly Pipe _appToBrowser = new(); + private readonly Stream _browserInput; + private readonly Stream _browserOutput; + private readonly Stream _browserRead; + private readonly Stream _browserWrite; + private readonly Pipe _browserToApp = new(); + private readonly TaskCompletionSource _processCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public FakePipeBrowserProcess() + { + _browserInput = _appToBrowser.Writer.AsStream(); + _browserRead = _appToBrowser.Reader.AsStream(); + _browserOutput = _browserToApp.Reader.AsStream(); + _browserWrite = _browserToApp.Writer.AsStream(); + } + + public int ProcessId => 4242; + + public Stream BrowserOutput => _browserOutput; + + public Stream BrowserInput => _browserInput; + + public Task ProcessTask => _processCompletion.Task; + + public bool Disposed { get; private set; } + + public async Task ReadFrameAsync() + { + using var frame = new MemoryStream(); + var oneByte = new byte[1]; + + while (true) + { + var read = await _browserRead.ReadAsync(oneByte); + if (read == 0) + { + throw new EndOfStreamException("The host pipe closed before a CDP frame was written."); + } + + if (oneByte[0] == 0) + { + return frame.ToArray(); + } + + frame.WriteByte(oneByte[0]); + } + } + + public async Task SendFrameAsync(string frame) + { + await _browserWrite.WriteAsync(Encoding.UTF8.GetBytes(frame)); + await _browserWrite.WriteAsync(new byte[] { 0 }); + await _browserWrite.FlushAsync(); + } + + public async ValueTask DisposeAsync() + { + if (Disposed) + { + return; + } + + Disposed = true; + _processCompletion.TrySetResult(new BrowserLogsProcessResult(0)); + + await _browserInput.DisposeAsync(); + await _browserOutput.DisposeAsync(); + await _browserRead.DisposeAsync(); + await _browserWrite.DisposeAsync(); + } + } +} diff --git a/tests/Aspire.Hosting.Tests/BrowserLogsBuilderExtensionsTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsBuilderExtensionsTests.cs similarity index 98% rename from tests/Aspire.Hosting.Tests/BrowserLogsBuilderExtensionsTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/BrowserLogsBuilderExtensionsTests.cs index 475fb99739d..c6bfe45f596 100644 --- a/tests/Aspire.Hosting.Tests/BrowserLogsBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsBuilderExtensionsTests.cs @@ -9,7 +9,9 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; -using Aspire.Hosting.Resources; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Tests; +using Aspire.Hosting.Browsers.Resources; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Aspire.Hosting.Eventing; @@ -19,7 +21,7 @@ using Microsoft.Extensions.Logging; using HealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class BrowserLogsBuilderExtensionsTests(ITestOutputHelper testOutputHelper) @@ -56,14 +58,14 @@ public void WithBrowserLogs_CreatesChildResource() Assert.Equal(web.Resource.Name, parentRelationship.Resource.Name); var command = Assert.Single(browserLogsResource.Annotations.OfType(), annotation => annotation.Name == BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName); - Assert.Equal(CommandStrings.OpenTrackedBrowserName, command.DisplayName); - Assert.Equal(CommandStrings.OpenTrackedBrowserDescription, command.DisplayDescription); + Assert.Equal(BrowserCommandStrings.OpenTrackedBrowserName, command.DisplayName); + Assert.Equal(BrowserCommandStrings.OpenTrackedBrowserDescription, command.DisplayDescription); var configureCommand = Assert.Single(browserLogsResource.Annotations.OfType(), annotation => annotation.Name == BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName); - Assert.Equal(CommandStrings.ConfigureTrackedBrowserName, configureCommand.DisplayName); - Assert.Equal(CommandStrings.ConfigureTrackedBrowserDescription, configureCommand.DisplayDescription); + Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserName, configureCommand.DisplayName); + Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserDescription, configureCommand.DisplayDescription); var screenshotCommand = Assert.Single(browserLogsResource.Annotations.OfType(), annotation => annotation.Name == BrowserLogsBuilderExtensions.CaptureScreenshotCommandName); - Assert.Equal(CommandStrings.CaptureScreenshotName, screenshotCommand.DisplayName); - Assert.Equal(CommandStrings.CaptureScreenshotDescription, screenshotCommand.DisplayDescription); + Assert.Equal(BrowserCommandStrings.CaptureScreenshotName, screenshotCommand.DisplayName); + Assert.Equal(BrowserCommandStrings.CaptureScreenshotDescription, screenshotCommand.DisplayDescription); var snapshot = browserLogsResource.Annotations.OfType().Single().InitialSnapshot; Assert.Equal(BrowserLogsBuilderExtensions.BrowserResourceType, snapshot.ResourceType); @@ -366,9 +368,9 @@ public async Task WithBrowserLogs_ConfigureCommandSavesResourceScopedBrowserSett var commandTask = app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName); var interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout(); - Assert.Equal(CommandStrings.ConfigureTrackedBrowserName, interaction.Title); - Assert.Equal(CommandStrings.ConfigureTrackedBrowserPromptMessage, interaction.Message); - Assert.Equal(CommandStrings.ConfigureTrackedBrowserSaveButton, ((InputsDialogInteractionOptions)interaction.Options!).PrimaryButtonText); + Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserName, interaction.Title); + Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserPromptMessage, interaction.Message); + Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserSaveButton, ((InputsDialogInteractionOptions)interaction.Options!).PrimaryButtonText); Assert.Collection(interaction.Inputs, input => Assert.Equal("scope", input.Name), input => Assert.Equal("browser", input.Name), @@ -379,7 +381,7 @@ public async Task WithBrowserLogs_ConfigureCommandSavesResourceScopedBrowserSett Assert.Equal("saveToUserSecrets", input.Name); Assert.Equal("true", input.Value); Assert.False(input.Disabled); - Assert.Equal(CommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured, input.Description); + Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionConfigured, input.Description); }); interaction.Inputs["scope"].Value = "resource"; @@ -438,7 +440,7 @@ public async Task WithBrowserLogs_ConfigureCommandAppliesRuntimeSettingsWhenUser var saveToUserSecrets = interaction.Inputs["saveToUserSecrets"]; Assert.True(saveToUserSecrets.Disabled); Assert.Null(saveToUserSecrets.Value); - Assert.Equal(CommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured, saveToUserSecrets.Description); + Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured, saveToUserSecrets.Description); interaction.Inputs["scope"].Value = "resource"; interaction.Inputs["browser"].Value = "msedge"; @@ -449,7 +451,7 @@ public async Task WithBrowserLogs_ConfigureCommandAppliesRuntimeSettingsWhenUser var result = await commandTask.DefaultTimeout(); Assert.True(result.Success); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, CommandStrings.ConfigureTrackedBrowserApplied, "web"), result.Message); + Assert.Equal(string.Format(CultureInfo.CurrentCulture, BrowserCommandStrings.ConfigureTrackedBrowserApplied, "web"), result.Message); Assert.Empty(userSecretsManager.Secrets); Assert.Empty(userSecretsManager.DeletedSecrets); diff --git a/tests/Aspire.Hosting.Tests/BrowserLogsCdpConnectionTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsCdpConnectionTests.cs similarity index 58% rename from tests/Aspire.Hosting.Tests/BrowserLogsCdpConnectionTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/BrowserLogsCdpConnectionTests.cs index f367030d3bb..668fe4373e4 100644 --- a/tests/Aspire.Hosting.Tests/BrowserLogsCdpConnectionTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsCdpConnectionTests.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging.Abstractions; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class BrowserLogsCdpConnectionTests @@ -146,10 +146,167 @@ await SendTextAsync( await pair.ServerSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Done", CancellationToken.None).DefaultTimeout(); } + [Fact] + public async Task CreateWithPipeTransport_UsesNullDelimitedFrames() + { + var appToBrowser = new Pipe(); + var browserToApp = new Pipe(); + await using var browserRead = appToBrowser.Reader.AsStream(); + await using var browserWrite = browserToApp.Writer.AsStream(); + var routedEventSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var connection = BrowserLogsCdpConnection.Create( + new BrowserLogsPipeCdpTransport(browserToApp.Reader.AsStream(), appToBrowser.Writer.AsStream()), + protocolEvent => + { + routedEventSource.TrySetResult(protocolEvent); + return ValueTask.CompletedTask; + }, + NullLogger.Instance); + + var createTargetTask = connection.CreateTargetAsync(CancellationToken.None); + var command = ParseReceivedCommand(await ReceiveNullTerminatedFrameAsync(browserRead).DefaultTimeout()); + Assert.Equal(BrowserLogsCdpProtocol.TargetCreateTargetMethod, command.Method); + Assert.Equal("about:blank", command.Url); + + await SendNullTerminatedFramesAsync( + browserWrite, + """ + { + "method": "Runtime.consoleAPICalled", + "sessionId": "target-session-1", + "params": { + "type": "log", + "args": [] + } + } + """, + $$""" + { + "id": {{command.Id}}, + "result": { + "targetId": "created-target" + } + } + """).DefaultTimeout(); + + var routedEvent = Assert.IsType(await routedEventSource.Task.DefaultTimeout()); + Assert.Equal("target-session-1", routedEvent.SessionId); + Assert.Equal("log", routedEvent.Parameters.Type); + + var result = await createTargetTask.DefaultTimeout(); + Assert.Equal("created-target", result.TargetId); + } + + [Fact] + public async Task MultiplexerLeasesShareCommandsAndBroadcastEvents() + { + FakeSharedCdpConnection? innerConnection = null; + await using var multiplexer = new BrowserLogsCdpConnectionMultiplexer( + eventHandler => + { + innerConnection = new FakeSharedCdpConnection(eventHandler); + return innerConnection; + }, + NullLogger.Instance); + + var firstEvents = new List(); + var secondEvents = new List(); + await using var firstConnection = multiplexer.CreateConnection(protocolEvent => + { + firstEvents.Add(protocolEvent); + return ValueTask.CompletedTask; + }); + await using var secondConnection = multiplexer.CreateConnection(protocolEvent => + { + secondEvents.Add(protocolEvent); + return ValueTask.CompletedTask; + }); + + var result = await firstConnection.CreateTargetAsync(CancellationToken.None); + Assert.Equal("created-target", result.TargetId); + Assert.Equal(1, innerConnection!.CreateTargetCount); + + var firstEvent = CreateConsoleEvent("target-session-1"); + await innerConnection.RaiseEventAsync(firstEvent); + Assert.Same(firstEvent, Assert.Single(firstEvents)); + Assert.Same(firstEvent, Assert.Single(secondEvents)); + + await firstConnection.DisposeAsync(); + await firstConnection.Completion.DefaultTimeout(); + Assert.False(innerConnection.Disposed); + + var secondEvent = CreateConsoleEvent("target-session-2"); + await innerConnection.RaiseEventAsync(secondEvent); + Assert.Single(firstEvents); + Assert.Equal(2, secondEvents.Count); + Assert.Same(secondEvent, secondEvents[1]); + Assert.False(secondConnection.Completion.IsCompleted); + } + + [Fact] + public async Task MultiplexerFaultsOnlyFailingSubscriberWhenEventHandlerThrows() + { + FakeSharedCdpConnection? innerConnection = null; + await using var multiplexer = new BrowserLogsCdpConnectionMultiplexer( + eventHandler => + { + innerConnection = new FakeSharedCdpConnection(eventHandler); + return innerConnection; + }, + NullLogger.Instance); + + await using var failingConnection = multiplexer.CreateConnection(_ => throw new InvalidOperationException("boom")); + var survivingEvents = new List(); + await using var survivingConnection = multiplexer.CreateConnection(protocolEvent => + { + survivingEvents.Add(protocolEvent); + return ValueTask.CompletedTask; + }); + + var protocolEvent = CreateConsoleEvent("target-session-1"); + await innerConnection!.RaiseEventAsync(protocolEvent); + + var exception = await Assert.ThrowsAsync(() => failingConnection.Completion.DefaultTimeout()); + Assert.Equal("Tracked browser CDP event handler failed.", exception.Message); + Assert.Same(protocolEvent, Assert.Single(survivingEvents)); + Assert.False(survivingConnection.Completion.IsCompleted); + await Assert.ThrowsAsync(() => failingConnection.CreateTargetAsync(CancellationToken.None)); + } + + [Fact] + public async Task MultiplexerRejectsNewLeasesAfterInnerConnectionCompletes() + { + FakeSharedCdpConnection? innerConnection = null; + await using var multiplexer = new BrowserLogsCdpConnectionMultiplexer( + eventHandler => + { + innerConnection = new FakeSharedCdpConnection(eventHandler); + return innerConnection; + }, + NullLogger.Instance); + + innerConnection!.Complete(); + await multiplexer.Completion.DefaultTimeout(); + + var exception = Assert.Throws(() => multiplexer.CreateConnection(static _ => ValueTask.CompletedTask)); + Assert.Equal("Tracked browser CDP pipe is no longer active.", exception.Message); + } + private static async Task ReceiveCommandAsync(WebSocket socket) { using var document = await ReceiveJsonDocumentAsync(socket).DefaultTimeout(); - var root = document.RootElement; + return ParseReceivedCommand(document.RootElement); + } + + private static ReceivedCommand ParseReceivedCommand(byte[] json) + { + using var document = JsonDocument.Parse(json); + return ParseReceivedCommand(document.RootElement); + } + + private static ReceivedCommand ParseReceivedCommand(JsonElement root) + { var id = root.GetProperty("id").GetInt64(); var method = root.GetProperty("method").GetString()!; var sessionId = root.TryGetProperty("sessionId", out var sessionIdElement) @@ -174,6 +331,17 @@ private static async Task ReceiveCommandAsync(WebSocket socket) return new ReceivedCommand(id, method, sessionId, targetId, url, format, fromSurface); } + private static BrowserLogsConsoleApiCalledEvent CreateConsoleEvent(string sessionId) + { + return new BrowserLogsConsoleApiCalledEvent( + sessionId, + new BrowserLogsRuntimeConsoleApiCalledParameters + { + Type = "log", + Args = [] + }); + } + private static async Task ReceiveJsonDocumentAsync(WebSocket socket) { var buffer = new byte[1024]; @@ -200,8 +368,110 @@ private static Task SendTextAsync(WebSocket socket, string text) return socket.SendAsync(Encoding.UTF8.GetBytes(text), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None); } + private static async Task ReceiveNullTerminatedFrameAsync(Stream stream) + { + using var frame = new MemoryStream(); + var oneByte = new byte[1]; + + while (true) + { + var read = await stream.ReadAsync(oneByte); + if (read == 0) + { + throw new EndOfStreamException("The stream closed before a null-terminated frame was received."); + } + + if (oneByte[0] == 0) + { + return frame.ToArray(); + } + + frame.WriteByte(oneByte[0]); + } + } + + private static async Task SendNullTerminatedFramesAsync(Stream stream, params string[] frames) + { + foreach (var frame in frames) + { + await stream.WriteAsync(Encoding.UTF8.GetBytes(frame)); + await stream.WriteAsync(new byte[] { 0 }); + } + + await stream.FlushAsync(); + } + private sealed record ReceivedCommand(long Id, string Method, string? SessionId, string? TargetId, string? Url, string? Format, bool? FromSurface); + private sealed class FakeSharedCdpConnection(Func eventHandler) : IBrowserLogsCdpConnection + { + private readonly TaskCompletionSource _completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public int CreateTargetCount { get; private set; } + + public bool Disposed { get; private set; } + + public Task Completion => _completionSource.Task; + + public ValueTask RaiseEventAsync(BrowserLogsCdpProtocolEvent protocolEvent) + { + return eventHandler(protocolEvent); + } + + public void Complete() + { + _completionSource.TrySetResult(); + } + + public Task CreateTargetAsync(CancellationToken cancellationToken) + { + CreateTargetCount++; + return Task.FromResult(new BrowserLogsCreateTargetResult { TargetId = "created-target" }); + } + + public Task GetTargetsAsync(CancellationToken cancellationToken) + { + return Task.FromResult(new BrowserLogsGetTargetsResult { TargetInfos = [] }); + } + + public Task AttachToTargetAsync(string targetId, CancellationToken cancellationToken) + { + return Task.FromResult(new BrowserLogsAttachToTargetResult { SessionId = "attached-session" }); + } + + public Task CloseTargetAsync(string targetId, CancellationToken cancellationToken) + { + return Task.FromResult(BrowserLogsCommandAck.Instance); + } + + public Task EnableTargetDiscoveryAsync(CancellationToken cancellationToken) + { + return Task.FromResult(BrowserLogsCommandAck.Instance); + } + + public Task EnablePageInstrumentationAsync(string sessionId, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task CaptureScreenshotAsync(string sessionId, CancellationToken cancellationToken) + { + return Task.FromResult(new BrowserLogsCaptureScreenshotResult { Data = "image-data" }); + } + + public Task NavigateAsync(string sessionId, Uri url, CancellationToken cancellationToken) + { + return Task.FromResult(BrowserLogsCommandAck.Instance); + } + + public ValueTask DisposeAsync() + { + Disposed = true; + _completionSource.TrySetResult(); + return ValueTask.CompletedTask; + } + } + private sealed class ConnectedClientWebSocketConnector(WebSocket webSocket) : IClientWebSocketConnector { private readonly WebSocket _webSocket = webSocket; diff --git a/tests/Aspire.Hosting.Tests/BrowserLogsCdpProtocolTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsCdpProtocolTests.cs similarity index 99% rename from tests/Aspire.Hosting.Tests/BrowserLogsCdpProtocolTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/BrowserLogsCdpProtocolTests.cs index 99d5fd4c92c..39efb4059e4 100644 --- a/tests/Aspire.Hosting.Tests/BrowserLogsCdpProtocolTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsCdpProtocolTests.cs @@ -3,7 +3,7 @@ using System.Text; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class BrowserLogsCdpProtocolTests diff --git a/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsPipeBrowserProcessLauncherTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsPipeBrowserProcessLauncherTests.cs new file mode 100644 index 00000000000..cd6c5d86a15 --- /dev/null +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsPipeBrowserProcessLauncherTests.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.AspNetCore.InternalTesting; + +namespace Aspire.Hosting.Browsers.Tests; + +[Trait("Partition", "2")] +public class BrowserLogsPipeBrowserProcessLauncherTests +{ + [Fact] + public void CreatePipeArguments_AppendsRemoteDebuggingPipeArgument() + { + var originalArguments = new[] + { + "--user-data-dir=/tmp/aspire-browser", + "--new-window", + "about:blank" + }; + + var pipeArguments = BrowserLogsPipeBrowserProcessLauncher.CreatePipeArguments(originalArguments); + + Assert.Equal( + [ + "--user-data-dir=/tmp/aspire-browser", + "--new-window", + "about:blank", + "--remote-debugging-pipe" + ], + pipeArguments); + Assert.DoesNotContain("--remote-debugging-pipe", originalArguments); + } + + [Fact] + public void BuildWindowsCommandLine_QuotesExecutableAndArguments() + { + var commandLine = BrowserLogsPipeBrowserProcessLauncher.BuildWindowsCommandLine( + @"C:\Program Files\Browser\chrome.exe", + [ + "--flag", + @"--user-data-dir=C:\Users\Test User\Profile", + "quote\"value" + ]); + + Assert.Equal("\"C:\\Program Files\\Browser\\chrome.exe\" --flag \"--user-data-dir=C:\\Users\\Test User\\Profile\" \"quote\\\"value\"", commandLine); + } + + [Fact] + public async Task Start_MapsPosixPipeDescriptorsToChildFd3AndFd4() + { + if (OperatingSystem.IsWindows()) + { + return; + } + + await using var process = BrowserLogsPipeBrowserProcessLauncher.Start( + "/bin/sh", + [ + "-c", + "IFS= read -r line <&3; printf '%s' \"$line\" >&4", + "browserlogs-pipe-test" + ]); + + await process.BrowserInput.WriteAsync(Encoding.UTF8.GetBytes("ping\n")).DefaultTimeout(); + await process.BrowserInput.FlushAsync().DefaultTimeout(); + + var response = await ReadExactlyAsync(process.BrowserOutput, byteCount: 4).DefaultTimeout(); + Assert.Equal("ping", Encoding.UTF8.GetString(response)); + + var result = await process.ProcessTask.DefaultTimeout(); + Assert.Equal(0, result.ExitCode); + } + + private static async Task ReadExactlyAsync(Stream stream, int byteCount) + { + var buffer = new byte[byteCount]; + var offset = 0; + + while (offset < buffer.Length) + { + var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset)); + if (read == 0) + { + throw new EndOfStreamException("The process exited before the expected pipe response was read."); + } + + offset += read; + } + + return buffer; + } +} diff --git a/tests/Aspire.Hosting.Tests/BrowserLogsRunningSessionTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsRunningSessionTests.cs similarity index 94% rename from tests/Aspire.Hosting.Tests/BrowserLogsRunningSessionTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/BrowserLogsRunningSessionTests.cs index c6b8178c769..2b0c17c8570 100644 --- a/tests/Aspire.Hosting.Tests/BrowserLogsRunningSessionTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsRunningSessionTests.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only +#pragma warning disable ASPIREBROWSERLOGS001 // Type is for evaluation purposes only using Aspire.Hosting.Tests.Utils; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class BrowserLogsRunningSessionTests @@ -118,6 +120,12 @@ private sealed class TestBrowserHost(BrowserHostIdentity identity) : IBrowserHos public TestBrowserPageSession? PageSession { get; private set; } + public Task CreateCdpConnectionAsync( + Func eventHandler, + ILogger logger, + CancellationToken cancellationToken) => + throw new NotSupportedException(); + public Task CreatePageSessionAsync( string sessionId, Uri url, diff --git a/tests/Aspire.Hosting.Tests/BrowserLogsSessionManagerTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsSessionManagerTests.cs similarity index 97% rename from tests/Aspire.Hosting.Tests/BrowserLogsSessionManagerTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/BrowserLogsSessionManagerTests.cs index 1e186171552..44047c23ede 100644 --- a/tests/Aspire.Hosting.Tests/BrowserLogsSessionManagerTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserLogsSessionManagerTests.cs @@ -4,13 +4,15 @@ using System.Net; using System.Net.Sockets; using System.Text; +using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Tests.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; #pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only +#pragma warning disable ASPIREBROWSERLOGS001 // Type is for evaluation purposes only -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class BrowserLogsSessionManagerTests @@ -163,7 +165,7 @@ public async Task BrowserHostRegistry_RejectsDifferentProfileForSharedHost() } [Fact] - public async Task BrowserHostRegistry_AdoptsValidatedSharedEndpointMetadata() + public async Task BrowserHostRegistry_AdoptsValidatedSharedEndpointMetadataWhenAdoptionIsEnabled() { var userDataDirectory = Directory.CreateTempSubdirectory(); try @@ -185,7 +187,8 @@ await BrowserEndpointDiscovery.WriteAsync( NullLogger.Instance, TimeProvider.System, createUserDataDirectory: (configuration, _) => BrowserLogsUserDataDirectory.CreatePersistent(userDataDirectory.FullName, configuration.Profile), - createHostAsync: null); + createHostAsync: null, + enableEndpointMetadataAdoption: true); var lease = await registry.AcquireAsync( new BrowserConfiguration(browserExecutable, Profile: null, BrowserUserDataMode.Shared, AppHostKey: null), @@ -653,6 +656,12 @@ public TestBrowserHost(BrowserHostIdentity identity, string? profileDirectoryNam public Task Termination { get; } = Task.CompletedTask; + public Task CreateCdpConnectionAsync( + Func eventHandler, + ILogger logger, + CancellationToken cancellationToken) => + throw new NotSupportedException(); + public Task CreatePageSessionAsync( string sessionId, Uri url, diff --git a/tests/Aspire.Hosting.Tests/BrowserPageSessionTests.cs b/tests/Aspire.Hosting.Browsers.Tests/BrowserPageSessionTests.cs similarity index 95% rename from tests/Aspire.Hosting.Tests/BrowserPageSessionTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/BrowserPageSessionTests.cs index 851698b5a1c..5f534c369a7 100644 --- a/tests/Aspire.Hosting.Tests/BrowserPageSessionTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/BrowserPageSessionTests.cs @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class BrowserPageSessionTests @@ -53,7 +54,7 @@ public async Task StartAsync_ReusesStartupTargetAttachesInstrumentsNavigatesAndR "session-0001", new Uri("https://localhost:5001/"), new BrowserConnectionDiagnosticsLogger("session-0001", NullLogger.Instance), - CreateConnectionFactory(host, connection), + CreateConnectionFactory(connection), protocolEvent => { routedEvents.Add(protocolEvent); @@ -111,7 +112,7 @@ public async Task DisposeAsync_ClosesTrackedTarget() "session-0001", new Uri("https://localhost:5001/"), new BrowserConnectionDiagnosticsLogger("session-0001", NullLogger.Instance), - CreateConnectionFactory(host, connection), + CreateConnectionFactory(connection), static _ => ValueTask.CompletedTask, NullLogger.Instance, TimeProvider.System, @@ -151,7 +152,7 @@ public async Task CaptureScreenshotAsync_UsesCurrentTargetSession() "session-0001", new Uri("https://localhost:5001/"), new BrowserConnectionDiagnosticsLogger("session-0001", NullLogger.Instance), - CreateConnectionFactory(host, connection), + CreateConnectionFactory(connection), static _ => ValueTask.CompletedTask, NullLogger.Instance, TimeProvider.System, @@ -181,7 +182,7 @@ public async Task CaptureScreenshotAsync_IsCanceledWhenSessionIsDisposed() "session-0001", new Uri("https://localhost:5001/"), new BrowserConnectionDiagnosticsLogger("session-0001", NullLogger.Instance), - CreateConnectionFactory(host, connection), + CreateConnectionFactory(connection), static _ => ValueTask.CompletedTask, NullLogger.Instance, TimeProvider.System, @@ -219,7 +220,7 @@ public async Task MonitorAsync_ReconnectsToExistingTargetAfterConnectionLoss() "session-0001", new Uri("https://localhost:5001/"), new BrowserConnectionDiagnosticsLogger("session-0001", NullLogger.Instance), - CreateConnectionFactory(host, firstConnection, secondConnection), + CreateConnectionFactory(firstConnection, secondConnection), protocolEvent => { routedEvents.Add(protocolEvent); @@ -266,12 +267,11 @@ await secondConnection.RaiseEventAsync(new BrowserLogsTargetDestroyedEvent( await session.DisposeAsync(); } - private static BrowserLogsCdpConnectionFactory CreateConnectionFactory(TestBrowserHost host, params FakeBrowserLogsCdpConnection[] connections) + private static BrowserLogsCdpConnectionFactory CreateConnectionFactory(params FakeBrowserLogsCdpConnection[] connections) { var nextConnectionIndex = 0; - return (webSocketUri, eventHandler, _, _) => + return (eventHandler, _, _) => { - Assert.Equal(host.DebugEndpoint, webSocketUri); Assert.True(nextConnectionIndex < connections.Length); var connection = connections[nextConnectionIndex++]; connection.SetEventHandler(eventHandler); @@ -297,6 +297,12 @@ private sealed class TestBrowserHost : IBrowserHost public Task Termination => _terminationSource.Task; + public Task CreateCdpConnectionAsync( + Func eventHandler, + ILogger logger, + CancellationToken cancellationToken) => + throw new NotSupportedException(); + public Task CreatePageSessionAsync( string sessionId, Uri url, diff --git a/tests/Aspire.Hosting.Tests/ChromiumDevToolsActivePortParserTests.cs b/tests/Aspire.Hosting.Browsers.Tests/ChromiumDevToolsActivePortParserTests.cs similarity index 96% rename from tests/Aspire.Hosting.Tests/ChromiumDevToolsActivePortParserTests.cs rename to tests/Aspire.Hosting.Browsers.Tests/ChromiumDevToolsActivePortParserTests.cs index d2e7571b6e5..f971a755dca 100644 --- a/tests/Aspire.Hosting.Tests/ChromiumDevToolsActivePortParserTests.cs +++ b/tests/Aspire.Hosting.Browsers.Tests/ChromiumDevToolsActivePortParserTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Browsers.Tests; [Trait("Partition", "2")] public class ChromiumDevToolsActivePortParserTests diff --git a/tests/Aspire.Hosting.Browsers.Tests/ConsoleLoggingTestHelpers.cs b/tests/Aspire.Hosting.Browsers.Tests/ConsoleLoggingTestHelpers.cs new file mode 100644 index 00000000000..72205aebed6 --- /dev/null +++ b/tests/Aspire.Hosting.Browsers.Tests/ConsoleLoggingTestHelpers.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Tests.Utils; + +internal static class ConsoleLoggingTestHelpers +{ + private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(30); + + public static async Task> CaptureLogsAsync(ResourceLoggerService service, string resourceName, int targetLogCount, Action writeLogs) + { + var subscribedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var watchTask = WatchForLogsAsync(service.WatchAsync(resourceName), targetLogCount); + + _ = Task.Run(async () => + { + await foreach (var subscriber in service.WatchAnySubscribersAsync()) + { + if (subscriber.Name == resourceName && subscriber.AnySubscribers) + { + subscribedTcs.TrySetResult(); + return; + } + } + }); + + await subscribedTcs.Task.WaitAsync(s_defaultTimeout); + writeLogs(); + + return await watchTask.WaitAsync(s_defaultTimeout); + } + + public static Task> WatchForLogsAsync(IAsyncEnumerable> watchEnumerable, int targetLogCount) + { + return Task.Run(async () => + { + var logs = new List(); + await foreach (var log in watchEnumerable) + { + logs.AddRange(log); + if (logs.Count >= targetLogCount) + { + break; + } + } + + return (IReadOnlyList)logs; + }); + } + + public static ResourceLoggerService GetResourceLoggerService() + { + var service = new ResourceLoggerService(); + var timeProviderProperty = typeof(ResourceLoggerService).GetProperty("TimeProvider", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("ResourceLoggerService.TimeProvider was not found."); + + timeProviderProperty.SetValue(service, new TestTimeProvider()); + + return service; + } + + private sealed class TestTimeProvider : TimeProvider + { + public override DateTimeOffset GetUtcNow() + { + return new DateTimeOffset(2000, 12, 29, 20, 59, 59, TimeSpan.Zero); + } + } +} diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index c76b837de70..a65935ed11d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -12,14 +12,6 @@ import ( // Enums // ============================================================================ -// BrowserUserDataMode represents BrowserUserDataMode. -type BrowserUserDataMode string - -const ( - BrowserUserDataModeShared BrowserUserDataMode = "Shared" - BrowserUserDataModeIsolated BrowserUserDataMode = "Isolated" -) - // ContainerLifetime represents ContainerLifetime. type ContainerLifetime string @@ -727,27 +719,6 @@ func NewCSharpAppResource(handle *Handle, client *AspireClient) *CSharpAppResour } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *CSharpAppResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *CSharpAppResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ @@ -3035,27 +3006,6 @@ func NewContainerResource(handle *Handle, client *AspireClient) *ContainerResour } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *ContainerResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *ContainerResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ @@ -5111,27 +5061,6 @@ func NewDotnetToolResource(handle *Handle, client *AspireClient) *DotnetToolReso } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *DotnetToolResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *DotnetToolResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ @@ -7215,27 +7144,6 @@ func NewExecutableResource(handle *Handle, client *AspireClient) *ExecutableReso } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *ExecutableResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *ExecutableResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ @@ -12063,27 +11971,6 @@ func NewProjectResource(handle *Handle, client *AspireClient) *ProjectResource { } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *ProjectResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *ProjectResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ @@ -14020,27 +13907,6 @@ func NewTestDatabaseResource(handle *Handle, client *AspireClient) *TestDatabase } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *TestDatabaseResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *TestDatabaseResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ @@ -15728,27 +15594,6 @@ func NewTestRedisResource(handle *Handle, client *AspireClient) *TestRedisResour } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *TestRedisResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *TestRedisResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ @@ -17591,27 +17436,6 @@ func NewTestVaultResource(handle *Handle, client *AspireClient) *TestVaultResour } } -// WithBrowserLogs adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. -func (s *TestVaultResource) WithBrowserLogs(browser *string, profile *string, userDataMode *BrowserUserDataMode) (*IResourceWithEndpoints, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - if browser != nil { - reqArgs["browser"] = SerializeValue(browser) - } - if profile != nil { - reqArgs["profile"] = SerializeValue(profile) - } - if userDataMode != nil { - reqArgs["userDataMode"] = SerializeValue(userDataMode) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEndpoints), nil -} - // WithContainerRegistry configures a resource to use a container registry func (s *TestVaultResource) WithContainerRegistry(registry *IResource) (*IResource, error) { reqArgs := map[string]any{ diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index e4fde40e103..7abe7db08b1 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1396,35 +1396,6 @@ public DistributedApplicationModel model() { } -// ===== BrowserUserDataMode.java ===== -// BrowserUserDataMode.java - GENERATED CODE - DO NOT EDIT - -package aspire; - -import java.util.*; -import java.util.function.*; - -/** BrowserUserDataMode enum. */ -public enum BrowserUserDataMode implements WireValueEnum { - SHARED("Shared"), - ISOLATED("Isolated"); - - private final String value; - - BrowserUserDataMode(String value) { - this.value = value; - } - - public String getValue() { return value; } - - public static BrowserUserDataMode fromValue(String value) { - for (BrowserUserDataMode e : values()) { - if (e.value.equals(value)) return e; - } - throw new IllegalArgumentException("Unknown value: " + value); - } -} - // ===== BuildOptions.java ===== // BuildOptions.java - GENERATED CODE - DO NOT EDIT @@ -1466,35 +1437,6 @@ public class CSharpAppResource extends ProjectResource { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public CSharpAppResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public CSharpAppResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private CSharpAppResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public CSharpAppResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -4316,35 +4258,6 @@ public class ContainerResource extends ResourceBuilderBase { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public ContainerResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public ContainerResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private ContainerResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public ContainerResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -6568,35 +6481,6 @@ public class DotnetToolResource extends ExecutableResource { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public DotnetToolResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public DotnetToolResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private DotnetToolResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public DotnetToolResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -8635,35 +8519,6 @@ public class ExecutableResource extends ResourceBuilderBase { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public ExecutableResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public ExecutableResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private ExecutableResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public ExecutableResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -13964,35 +13819,6 @@ public class ProjectResource extends ResourceBuilderBase { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public ProjectResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public ProjectResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private ProjectResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public ProjectResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -16399,35 +16225,6 @@ public class TestDatabaseResource extends ContainerResource { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public TestDatabaseResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public TestDatabaseResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private TestDatabaseResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public TestDatabaseResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -18288,35 +18085,6 @@ public class TestRedisResource extends ContainerResource { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public TestRedisResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public TestRedisResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private TestRedisResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public TestRedisResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -20270,35 +20038,6 @@ public class TestVaultResource extends ContainerResource { super(handle, client); } - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - public TestVaultResource withBrowserLogs(WithBrowserLogsOptions options) { - var browser = options == null ? null : options.getBrowser(); - var profile = options == null ? null : options.getProfile(); - var userDataMode = options == null ? null : options.getUserDataMode(); - return withBrowserLogsImpl(browser, profile, userDataMode); - } - - public TestVaultResource withBrowserLogs() { - return withBrowserLogs(null); - } - - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - private TestVaultResource withBrowserLogsImpl(String browser, String profile, BrowserUserDataMode userDataMode) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (browser != null) { - reqArgs.put("browser", AspireClient.serializeValue(browser)); - } - if (profile != null) { - reqArgs.put("profile", AspireClient.serializeValue(profile)); - } - if (userDataMode != null) { - reqArgs.put("userDataMode", AspireClient.serializeValue(userDataMode)); - } - getClient().invokeCapability("Aspire.Hosting/withBrowserLogs", reqArgs); - return this; - } - /** Configures a resource to use a container registry */ public TestVaultResource withContainerRegistry(IResource registry) { Map reqArgs = new HashMap<>(); @@ -22137,40 +21876,6 @@ public interface WireValueEnum { String getValue(); } -// ===== WithBrowserLogsOptions.java ===== -// WithBrowserLogsOptions.java - GENERATED CODE - DO NOT EDIT - -package aspire; - -import java.util.*; -import java.util.function.*; - -/** Options for WithBrowserLogs. */ -public final class WithBrowserLogsOptions { - private String browser; - private String profile; - private BrowserUserDataMode userDataMode; - - public String getBrowser() { return browser; } - public WithBrowserLogsOptions browser(String value) { - this.browser = value; - return this; - } - - public String getProfile() { return profile; } - public WithBrowserLogsOptions profile(String value) { - this.profile = value; - return this; - } - - public BrowserUserDataMode getUserDataMode() { return userDataMode; } - public WithBrowserLogsOptions userDataMode(BrowserUserDataMode value) { - this.userDataMode = value; - return this; - } - -} - // ===== WithContainerCertificatePathsOptions.java ===== // WithContainerCertificatePathsOptions.java - GENERATED CODE - DO NOT EDIT @@ -22837,7 +22542,6 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/BaseRegistrations.java .modules/BeforeResourceStartedEvent.java .modules/BeforeStartEvent.java -.modules/BrowserUserDataMode.java .modules/BuildOptions.java .modules/CSharpAppResource.java .modules/CancellationToken.java @@ -22974,7 +22678,6 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/WellKnownPipelineSteps.java .modules/WellKnownPipelineTags.java .modules/WireValueEnum.java -.modules/WithBrowserLogsOptions.java .modules/WithContainerCertificatePathsOptions.java .modules/WithDataVolumeOptions.java .modules/WithDockerfileBaseImageOptions.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 5f5248f6e38..94ade19a23c 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1495,8 +1495,6 @@ def _validate_dict_types(args: typing.Any, arg_types: typing.Any) -> bool: # Enum Types # ============================================================================ -BrowserUserDataMode = typing.Literal["Shared", "Isolated"] - CertificateTrustScope = typing.Literal["None", "Append", "Override", "System"] CommandResultFormat = typing.Literal["Text", "Json", "Markdown"] @@ -1580,12 +1578,6 @@ class MergeRouteParameters(typing.TypedDict, total=False): middleware: str -class BrowserLogsParameters(typing.TypedDict, total=False): - browser: str - profile: str - user_data_mode: BrowserUserDataMode - - class BindMountParameters(typing.TypedDict, total=False): source: typing.Required[str] target: typing.Required[str] @@ -5877,10 +5869,6 @@ def clear_container_files_sources(self) -> typing.Self: class AbstractResourceWithEndpoints(AbstractResource): """Abstract base class for AbstractResourceWithEndpoints interface.""" - @abc.abstractmethod - def with_browser_logs(self, *, browser: str | None = None, profile: str | None = None, user_data_mode: BrowserUserDataMode | None = None) -> typing.Self: - """Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots.""" - @abc.abstractmethod def with_mcp_server(self, *, path: str = "/mcp", endpoint_name: str | None = None) -> typing.Self: """Configures an MCP server endpoint on the resource""" @@ -6937,7 +6925,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ContainerResourceKwargs(_BaseResourceKwargs, total=False): """ContainerResource options.""" - browser_logs: BrowserLogsParameters | typing.Literal[True] bind_mount: tuple[str, str] | BindMountParameters entrypoint: str image_tag: str @@ -6997,22 +6984,6 @@ class ContainerResource(_BaseResource, AbstractResourceWithEnvironment, Abstract def __repr__(self) -> str: return "ContainerResource(handle={self._handle.handle_id})" - def with_browser_logs(self, *, browser: str | None = None, profile: str | None = None, user_data_mode: BrowserUserDataMode | None = None) -> typing.Self: - """Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots.""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - if browser is not None: - rpc_args['browser'] = browser - if profile is not None: - rpc_args['profile'] = profile - if user_data_mode is not None: - rpc_args['userDataMode'] = user_data_mode - result = self._client.invoke_capability( - 'Aspire.Hosting/withBrowserLogs', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_bind_mount(self, source: str, target: str, *, is_read_only: bool = False) -> typing.Self: """Adds a bind mount""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7698,18 +7669,6 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ContainerResourceKwargs]) -> None: - if _browser_logs := kwargs.pop("browser_logs", None): - if _validate_dict_types(_browser_logs, BrowserLogsParameters): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["browser"] = typing.cast(BrowserLogsParameters, _browser_logs).get("browser") - rpc_args["profile"] = typing.cast(BrowserLogsParameters, _browser_logs).get("profile") - rpc_args["userDataMode"] = typing.cast(BrowserLogsParameters, _browser_logs).get("user_data_mode") - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withBrowserLogs', rpc_args)) - elif _browser_logs is True: - rpc_args: dict[str, typing.Any] = {"builder": handle} - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withBrowserLogs', rpc_args)) - else: - raise TypeError("Invalid type for option 'browser_logs'. Expected: BrowserLogsParameters or Literal[True]") if _bind_mount := kwargs.pop("bind_mount", None): if _validate_tuple_types(_bind_mount, (str, str)): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8212,7 +8171,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ProjectResourceKwargs(_BaseResourceKwargs, total=False): """ProjectResource options.""" - browser_logs: BrowserLogsParameters | typing.Literal[True] mcp_server: McpServerParameters | typing.Literal[True] otlp_exporter: OtlpProtocol | typing.Literal[True] replicas: int @@ -8256,22 +8214,6 @@ class ProjectResource(_BaseResource, AbstractResourceWithEnvironment, AbstractRe def __repr__(self) -> str: return "ProjectResource(handle={self._handle.handle_id})" - def with_browser_logs(self, *, browser: str | None = None, profile: str | None = None, user_data_mode: BrowserUserDataMode | None = None) -> typing.Self: - """Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots.""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - if browser is not None: - rpc_args['browser'] = browser - if profile is not None: - rpc_args['profile'] = profile - if user_data_mode is not None: - rpc_args['userDataMode'] = user_data_mode - result = self._client.invoke_capability( - 'Aspire.Hosting/withBrowserLogs', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_mcp_server(self, *, path: str = "/mcp", endpoint_name: str | None = None) -> typing.Self: """Configures an MCP server endpoint on the resource""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -8761,18 +8703,6 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ProjectResourceKwargs]) -> None: - if _browser_logs := kwargs.pop("browser_logs", None): - if _validate_dict_types(_browser_logs, BrowserLogsParameters): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["browser"] = typing.cast(BrowserLogsParameters, _browser_logs).get("browser") - rpc_args["profile"] = typing.cast(BrowserLogsParameters, _browser_logs).get("profile") - rpc_args["userDataMode"] = typing.cast(BrowserLogsParameters, _browser_logs).get("user_data_mode") - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withBrowserLogs', rpc_args)) - elif _browser_logs is True: - rpc_args: dict[str, typing.Any] = {"builder": handle} - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withBrowserLogs', rpc_args)) - else: - raise TypeError("Invalid type for option 'browser_logs'. Expected: BrowserLogsParameters or Literal[True]") if _mcp_server := kwargs.pop("mcp_server", None): if _validate_dict_types(_mcp_server, McpServerParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9144,7 +9074,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): """ExecutableResource options.""" - browser_logs: BrowserLogsParameters | typing.Literal[True] publish_as_docker_file: typing.Callable[[ContainerResource], None] executable_command: str working_dir: str @@ -9187,22 +9116,6 @@ class ExecutableResource(_BaseResource, AbstractResourceWithEnvironment, Abstrac def __repr__(self) -> str: return "ExecutableResource(handle={self._handle.handle_id})" - def with_browser_logs(self, *, browser: str | None = None, profile: str | None = None, user_data_mode: BrowserUserDataMode | None = None) -> typing.Self: - """Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots.""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - if browser is not None: - rpc_args['browser'] = browser - if profile is not None: - rpc_args['profile'] = profile - if user_data_mode is not None: - rpc_args['userDataMode'] = user_data_mode - result = self._client.invoke_capability( - 'Aspire.Hosting/withBrowserLogs', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def publish_as_docker_file(self, configure: typing.Callable[[ContainerResource], None]) -> typing.Self: """Publishes an executable as a Docker file""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9680,18 +9593,6 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ExecutableResourceKwargs]) -> None: - if _browser_logs := kwargs.pop("browser_logs", None): - if _validate_dict_types(_browser_logs, BrowserLogsParameters): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["browser"] = typing.cast(BrowserLogsParameters, _browser_logs).get("browser") - rpc_args["profile"] = typing.cast(BrowserLogsParameters, _browser_logs).get("profile") - rpc_args["userDataMode"] = typing.cast(BrowserLogsParameters, _browser_logs).get("user_data_mode") - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withBrowserLogs', rpc_args)) - elif _browser_logs is True: - rpc_args: dict[str, typing.Any] = {"builder": handle} - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withBrowserLogs', rpc_args)) - else: - raise TypeError("Invalid type for option 'browser_logs'. Expected: BrowserLogsParameters or Literal[True]") if _publish_as_docker_file := kwargs.pop("publish_as_docker_file", None): if _validate_type(_publish_as_docker_file, typing.Callable[[ContainerResource], None]): rpc_args: dict[str, typing.Any] = {"builder": handle} diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index f62c3b1d388..bcda4b00ae6 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -20,25 +20,6 @@ use crate::base::{ // Enums // ============================================================================ -/// BrowserUserDataMode -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum BrowserUserDataMode { - #[default] - #[serde(rename = "Shared")] - Shared, - #[serde(rename = "Isolated")] - Isolated, -} - -impl std::fmt::Display for BrowserUserDataMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Shared => write!(f, "Shared"), - Self::Isolated => write!(f, "Isolated"), - } - } -} - /// ContainerLifetime #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ContainerLifetime { @@ -1213,24 +1194,6 @@ impl CSharpAppResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3192,24 +3155,6 @@ impl ContainerResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4998,24 +4943,6 @@ impl DotnetToolResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); @@ -6730,24 +6657,6 @@ impl ExecutableResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); @@ -11252,24 +11161,6 @@ impl ProjectResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12992,24 +12883,6 @@ impl TestDatabaseResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14386,24 +14259,6 @@ impl TestRedisResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15886,24 +15741,6 @@ impl TestVaultResource { &self.client } - /// Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. - pub fn with_browser_logs(&self, browser: Option<&str>, profile: Option<&str>, user_data_mode: Option) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - if let Some(ref v) = browser { - args.insert("browser".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = profile { - args.insert("profile".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - if let Some(ref v) = user_data_mode { - args.insert("userDataMode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - let result = self.client.invoke_capability("Aspire.Hosting/withBrowserLogs", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Configures a resource to use a container registry pub fn with_container_registry(&self, registry: &IResource) -> Result> { let mut args: HashMap = HashMap::new(); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj index f4d5d704e61..a46d60b1dab 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs index a057041c758..47d76da7cbc 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.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 ASPIREBROWSERLOGS001 // Type is for evaluation purposes only + using System.Reflection; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.RemoteHost; @@ -397,16 +399,17 @@ public async Task Scanner_HostingAssembly_AddContainerCapability() } [Fact] - public void Scanner_HostingAssembly_WithBrowserLogsCapability() + public void Scanner_BrowsersAssembly_WithBrowserLogsCapability() { - var capabilities = ScanCapabilitiesFromHostingAssembly(); + var capabilities = ScanCapabilitiesFromBrowsersAssembly(); - var withBrowserLogs = capabilities.FirstOrDefault(c => c.CapabilityId == "Aspire.Hosting/withBrowserLogs"); + var withBrowserLogs = capabilities.FirstOrDefault(c => c.CapabilityId == "Aspire.Hosting.Browsers/withBrowserLogs"); Assert.NotNull(withBrowserLogs); Assert.Equal("withBrowserLogs", withBrowserLogs.MethodName); Assert.Equal("Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints", withBrowserLogs.TargetTypeId); Assert.Contains(withBrowserLogs.Parameters, p => p.Name == "browser" && p.Type?.TypeId == "string" && p.IsOptional); Assert.Contains(withBrowserLogs.Parameters, p => p.Name == "profile" && p.Type?.TypeId == "string" && p.IsOptional); + Assert.Contains(withBrowserLogs.Parameters, p => p.Name == "userDataMode" && p.IsOptional); Assert.True(withBrowserLogs.ReturnsBuilder); } @@ -926,6 +929,13 @@ private static List ScanCapabilitiesFromHostingAssembly() return result.Capabilities; } + private static List ScanCapabilitiesFromBrowsersAssembly() + { + var browsersAssembly = typeof(global::Aspire.Hosting.BrowserLogsBuilderExtensions).Assembly; + var result = AtsCapabilityScanner.ScanAssembly(browsersAssembly); + return result.Capabilities; + } + private static AtsContext CreateContextFromHostingAssembly() { var hostingAssembly = typeof(DistributedApplication).Assembly; diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt index 84b6fd6c8bf..9c7ec0497ca 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt @@ -265,20 +265,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withBrowserLogs, - MethodName: withBrowserLogs, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withBuildArg, MethodName: withBuildArg, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 2a498bf3b06..fade7da509f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -290,12 +290,6 @@ type IServiceProviderHandle = Handle<'System.ComponentModel/System.IServiceProvi // Enum Types // ============================================================================ -/** Enum type for BrowserUserDataMode */ -export enum BrowserUserDataMode { - Shared = "Shared", - Isolated = "Isolated", -} - /** Enum type for CertificateTrustScope */ export enum CertificateTrustScope { None = "None", @@ -855,12 +849,6 @@ export interface WithBindMountOptions { isReadOnly?: boolean; } -export interface WithBrowserLogsOptions { - browser?: string; - profile?: string; - userDataMode?: BrowserUserDataMode; -} - export interface WithCommandOptions { commandOptions?: CommandOptions; } @@ -8778,8 +8766,6 @@ class ContainerRegistryResourcePromiseImpl implements ContainerRegistryResourceP export interface ContainerResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ContainerResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; /** Adds a bind mount */ @@ -8979,8 +8965,6 @@ export interface ContainerResource { } export interface ContainerResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ContainerResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; /** Adds a bind mount */ @@ -9188,26 +9172,6 @@ class ContainerResourceImpl extends ResourceBuilderBase super(handle, client); } - /** @internal */ - private async _withBrowserLogsInternal(browser?: string, profile?: string, userDataMode?: BrowserUserDataMode): Promise { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): ContainerResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new ContainerResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -10840,10 +10804,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -11244,8 +11204,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { export interface CSharpAppResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): CSharpAppResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): CSharpAppResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -11413,8 +11371,6 @@ export interface CSharpAppResource { } export interface CSharpAppResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): CSharpAppResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): CSharpAppResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -11590,26 +11546,6 @@ class CSharpAppResourceImpl extends ResourceBuilderBase super(handle, client); } - /** @internal */ - private async _withBrowserLogsInternal(browser?: string, profile?: string, userDataMode?: BrowserUserDataMode): Promise { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new CSharpAppResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): CSharpAppResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new CSharpAppResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -12999,10 +12935,6 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -13339,8 +13271,6 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { export interface DotnetToolResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): DotnetToolResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): DotnetToolResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -13518,8 +13448,6 @@ export interface DotnetToolResource { } export interface DotnetToolResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): DotnetToolResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): DotnetToolResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -13705,26 +13633,6 @@ class DotnetToolResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new DotnetToolResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): DotnetToolResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new DotnetToolResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -15181,10 +15089,6 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -15541,8 +15445,6 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { export interface ExecutableResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ExecutableResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): ExecutableResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -15708,8 +15610,6 @@ export interface ExecutableResource { } export interface ExecutableResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ExecutableResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): ExecutableResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -15883,26 +15783,6 @@ class ExecutableResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new ExecutableResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): ExecutableResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new ExecutableResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -17275,10 +17155,6 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -19899,8 +19775,6 @@ class ParameterResourcePromiseImpl implements ParameterResourcePromise { export interface ProjectResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ProjectResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): ProjectResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -20068,8 +19942,6 @@ export interface ProjectResource { } export interface ProjectResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ProjectResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): ProjectResourcePromise; /** Sets the base image for a Dockerfile build */ @@ -20245,26 +20117,6 @@ class ProjectResourceImpl extends ResourceBuilderBase imp super(handle, client); } - /** @internal */ - private async _withBrowserLogsInternal(browser?: string, profile?: string, userDataMode?: BrowserUserDataMode): Promise { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new ProjectResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): ProjectResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new ProjectResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -21654,10 +21506,6 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -21994,8 +21842,6 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { export interface TestDatabaseResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): TestDatabaseResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): TestDatabaseResourcePromise; /** Adds a bind mount */ @@ -22195,8 +22041,6 @@ export interface TestDatabaseResource { } export interface TestDatabaseResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): TestDatabaseResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): TestDatabaseResourcePromise; /** Adds a bind mount */ @@ -22404,26 +22248,6 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): TestDatabaseResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new TestDatabaseResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -24056,10 +23880,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -24460,8 +24280,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { export interface TestRedisResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): TestRedisResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; /** Adds a bind mount */ @@ -24691,8 +24509,6 @@ export interface TestRedisResource { } export interface TestRedisResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): TestRedisResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; /** Adds a bind mount */ @@ -24930,26 +24746,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase super(handle, client); } - /** @internal */ - private async _withBrowserLogsInternal(browser?: string, profile?: string, userDataMode?: BrowserUserDataMode): Promise { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): TestRedisResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new TestRedisResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -26780,10 +26576,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -27244,8 +27036,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { export interface TestVaultResource { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): TestVaultResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; /** Adds a bind mount */ @@ -27447,8 +27237,6 @@ export interface TestVaultResource { } export interface TestVaultResourcePromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): TestVaultResourcePromise; /** Configures a resource to use a container registry */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; /** Adds a bind mount */ @@ -27658,26 +27446,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase super(handle, client); } - /** @internal */ - private async _withBrowserLogsInternal(browser?: string, profile?: string, userDataMode?: BrowserUserDataMode): Promise { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): TestVaultResourcePromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new TestVaultResourcePromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withContainerRegistryInternal(registry: Awaitable): Promise { registry = isPromiseLike(registry) ? await registry : registry; @@ -29324,10 +29092,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withContainerRegistry(registry: Awaitable): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } @@ -31351,8 +31115,6 @@ class ResourceWithContainerFilesPromiseImpl implements ResourceWithContainerFile export interface ResourceWithEndpoints { toJSON(): MarshalledHandle; - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ResourceWithEndpointsPromise; /** Configures an MCP server endpoint on the resource */ withMcpServer(options?: WithMcpServerOptions): ResourceWithEndpointsPromise; /** Updates a named endpoint via callback */ @@ -31384,8 +31146,6 @@ export interface ResourceWithEndpoints { } export interface ResourceWithEndpointsPromise extends PromiseLike { - /** Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots. */ - withBrowserLogs(options?: WithBrowserLogsOptions): ResourceWithEndpointsPromise; /** Configures an MCP server endpoint on the resource */ withMcpServer(options?: WithMcpServerOptions): ResourceWithEndpointsPromise; /** Updates a named endpoint via callback */ @@ -31425,26 +31185,6 @@ class ResourceWithEndpointsImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle }; - if (browser !== undefined) rpcArgs.browser = browser; - if (profile !== undefined) rpcArgs.profile = profile; - if (userDataMode !== undefined) rpcArgs.userDataMode = userDataMode; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBrowserLogs', - rpcArgs - ); - return new ResourceWithEndpointsImpl(result, this._client); - } - - withBrowserLogs(options?: WithBrowserLogsOptions): ResourceWithEndpointsPromise { - const browser = options?.browser; - const profile = options?.profile; - const userDataMode = options?.userDataMode; - return new ResourceWithEndpointsPromiseImpl(this._withBrowserLogsInternal(browser, profile, userDataMode), this._client); - } - /** @internal */ private async _withMcpServerInternal(path?: string, endpointName?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -31745,10 +31485,6 @@ class ResourceWithEndpointsPromiseImpl implements ResourceWithEndpointsPromise { return this._promise.then(onfulfilled, onrejected); } - withBrowserLogs(options?: WithBrowserLogsOptions): ResourceWithEndpointsPromise { - return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withBrowserLogs(options)), this._client); - } - withMcpServer(options?: WithMcpServerOptions): ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withMcpServer(options)), this._client); } diff --git a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs index aa04479746a..d49da1a4b85 100644 --- a/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using HealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus; namespace Aspire.Hosting.Tests; @@ -137,6 +138,31 @@ async Task> GetValuesAsync(CancellationToken cancellationTok }); } + [Fact] + public async Task PublishedHealthReportsUpdateHealthStatus() + { + var resource = new CustomResource("myResource"); + var notificationService = ResourceNotificationServiceTestHelpers.Create(); + + await notificationService.PublishUpdateAsync(resource, snapshot => + (snapshot with { State = KnownResourceStates.Running }).WithHealthReports( + [ + new HealthReportSnapshot("browser-session", HealthStatus.Unhealthy, "Browser session failed.", "InvalidOperationException: Browser crashed.") + ])).DefaultTimeout(); + + Assert.True(notificationService.TryGetCurrentState(resource.Name, out var resourceEvent)); + Assert.Equal(HealthStatus.Unhealthy, resourceEvent.Snapshot.HealthStatus); + Assert.Collection( + resourceEvent.Snapshot.HealthReports, + report => + { + Assert.Equal("browser-session", report.Name); + Assert.Equal(HealthStatus.Unhealthy, report.Status); + Assert.Equal("Browser session failed.", report.Description); + Assert.Equal("InvalidOperationException: Browser crashed.", report.ExceptionText); + }); + } + [Fact] public async Task WatchingAllResourcesNotifiesOfAnyResourceChange() { From 44c4e3c1a373d4946812909ffa7ffe990bd2435c Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 13:54:14 -0700 Subject: [PATCH 17/55] [release/13.3] Gateway TLS without hostname (FQDN discovery) + TS AppHost endpoint fix (#16585) * Support Gateway TLS without pre-known hostname (FQDN discovery) When WithTls() is called without WithHostname(), the Gateway now: 1. Generates an HTTPS listener without a hostname restriction 2. After Helm deploy, polls Gateway status for the assigned address 3. Patches the HTTPS listener to add the discovered hostname 4. Creates a bootstrap self-signed TLS secret with the discovered FQDN 5. cert-manager then detects the hostname and issues a real certificate This enables a single-deploy TLS workflow for controllers like AGC that assign FQDNs automatically (e.g., *.alb.azure.com), without requiring users to deploy once to discover the FQDN and then redeploy with it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use helm field-manager for Gateway hostname patch to avoid conflicts After patching the Gateway hostname via JSON patch, re-apply the full Gateway YAML with --server-side --field-manager=helm --force-conflicts to transfer field ownership back to Helm. This prevents SSA conflicts when the user later redeploys with an explicit hostname via WithHostname(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix endpoint resource lookup for TS AppHost RPC bridge Use ResourceNameComparer on the deploymentTargets dictionary so that endpoint references created through the TypeScript AppHost RPC bridge (which may use a different resource instance) resolve correctly by resource name. This matches the pattern already used in KubernetesEnvironmentContext._kubernetesComponents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix test assertion for YAML quoted protocol values The YAML serializer quotes string values like protocol: "HTTPS" on CI. Use a more flexible assertion that matches both quoted and unquoted forms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address all automated review feedback 1. Address type validation: Parse full Gateway JSON status, prefer Hostname-type addresses, fall back to DNS-like values, skip IPs. 2. Use -o json instead of jsonpath: Parse full Gateway JSON for both address discovery and listener index detection. More reliable. 3. Use JsonSerializer + --patch-file: Build JSON patch operations with proper serialization, write to temp file to avoid shell escaping. 4. Minimal manifest for field ownership: Read current Gateway JSON, strip server fields (status, resourceVersion, managedFields), keep only apiVersion/kind/metadata(name,namespace)/spec for SSA apply. 5. Temp file pattern: Use CreateTempSubdirectory consistently. 6. Add SAN to bootstrap certs: Add SubjectAlternativeNameBuilder with DNS name in both DiscoverFqdnAndBootstrapTlsAsync and the existing BootstrapTlsSecretsAsync. Modern TLS clients require SAN. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve annotations and labels in Gateway ownership transfer The minimal manifest used for server-side apply field ownership transfer was missing annotations and labels, causing AGC annotations like alb.networking.azure.io/alb-name to be stripped. Now copies annotations and labels from the current Gateway metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add E2E test for K8S Gateway TLS deployment with HTTP-01 Tests the full flow: provision AKS with ALB controller, install cert-manager with gatewayHTTPRoute HTTP-01 solver, create a project with AddKubernetesEnvironment + AddGateway + WithTls (no hostname), deploy with aspire deploy, and verify FQDN discovery, certificate issuance, and HTTPS access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use --enable-alb in az aks create and AMD VM SKU Consolidate ALB enablement into the az aks create command instead of a separate az aks update step. Use Standard_D2as_v5 (AMD) for quota compatibility with E2E test subscription. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix E2E test: use --enable-gateway-api --enable-application-load-balancer Per the official AGC quickstart docs, use the correct flags: - --enable-gateway-api: enables Gateway API CRDs - --enable-application-load-balancer: enables ALB controller addon - --network-plugin azure: required Azure CNI - Standard_D2as_v5: AMD VM SKU for quota compatibility - Use the add-on's auto-created aks-appgateway subnet instead of creating one manually Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Wait for ALB controller pods to be Running before checking GatewayClass The ALB controller pods need time to initialize after cluster creation. Poll until pods are Running and GatewayClass azure-alb-external exists, with up to 10 minutes timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix ClusterIssuer: parentRefs requires name and namespace cert-manager requires parentRefs to include a name. Use 'ingress' to match the Gateway name from AddGateway('ingress'), and include the namespace to match the Helm deploy namespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix E2E test AppHost: add using directive and pragma suppressions The injected AppHost code needs: - using Aspire.Hosting.Kubernetes for AddGateway extension methods - #pragma warning disable ASPIRECOMPUTE003 for AddContainerRegistry Both prepended to the top of the file alongside ASPIREPIPELINES001. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix E2E test: capture webfrontend variable from starter template The starter template generates builder.AddProject('webfrontend') without assigning to a variable. The Gateway route needs a reference to it, so inject 'var webfrontend =' before the AddProject call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor FQDN discovery polling to use Polly retry pipeline Replace the manual for-loop retry with a Polly ResiliencePipeline using constant 5s backoff, 60 max attempts, and result-based retry (retries when result is null). Polly.Core is already a transitive dependency via Aspire.Hosting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Mitch Denny Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../KubernetesEnvironmentResource.cs | 591 +++++++++++++++++- .../KubernetesGatewayTlsDeploymentTests.cs | 504 +++++++++++++++ .../KubernetesGatewayTests.cs | 42 ++ 3 files changed, 1130 insertions(+), 7 deletions(-) create mode 100644 tests/Aspire.Deployment.EndToEnd.Tests/KubernetesGatewayTlsDeploymentTests.cs diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs index 8aa7e3fabc5..0160498f2d8 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs @@ -14,6 +14,8 @@ using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Polly; +using Polly.Retry; namespace Aspire.Hosting.Kubernetes; @@ -239,6 +241,27 @@ public KubernetesEnvironmentResource(string name) : base(name) steps.Add(tlsBootstrapStep); } + // FQDN discovery step — for Gateway TLS configs with no hostnames, waits for the + // Gateway to be assigned an address, patches the listener hostname, and bootstraps TLS. + var gatewaysNeedingDiscovery = CollectGatewaysNeedingFqdnDiscovery(model, environment); + if (gatewaysNeedingDiscovery.Count > 0) + { + var fqdnDiscoveryStep = new PipelineStep + { + Name = $"tls-fqdn-discovery-{environment.Name}", + Description = "Discovers Gateway FQDN, patches listener hostname, and bootstraps TLS", + Action = ctx => DiscoverFqdnAndBootstrapTlsAsync(ctx, environment, gatewaysNeedingDiscovery) + }; + fqdnDiscoveryStep.DependsOn($"helm-deploy-{environment.Name}"); + if (tlsSecrets.Count > 0) + { + // Run after normal TLS bootstrap (which handles hostnames that are already known) + fqdnDiscoveryStep.DependsOn($"tls-bootstrap-{environment.Name}"); + } + fqdnDiscoveryStep.RequiredBy(WellKnownPipelineSteps.Deploy); + steps.Add(fqdnDiscoveryStep); + } + // Expand deployment target steps for compute resources (including dashboard if enabled) var resources = environment.DashboardEnabled && environment.Dashboard?.Resource is KubernetesAspireDashboardResource dashboard ? [.. model.GetComputeResources(), dashboard] @@ -365,8 +388,11 @@ private async Task PrepareDeploymentTargetsAsync(PipelineStepContext context) }); } - // Build deployment target lookup for endpoint resolution - var deploymentTargets = new Dictionary(); + // Build deployment target lookup for endpoint resolution. + // Use name-based equality so that endpoint references created through the + // TypeScript AppHost RPC bridge (which may use a different resource instance) + // resolve correctly. + var deploymentTargets = new Dictionary(new ResourceNameComparer()); foreach (var r in appModel.GetComputeResources()) { @@ -719,20 +745,21 @@ private static async Task BuildGatewayObjects( var tlsListenerIndex = 0; foreach (var tls in gatewayResource.TlsConfigs) { - foreach (var host in tls.Hosts) + var resolvedSecretName = await ResolveExpressionAsync(tls.SecretName, cancellationToken).ConfigureAwait(false); + + if (tls.Hosts.Count == 0) { + // No hostnames specified — create an HTTPS listener without a hostname restriction. + // The hostname will be discovered from the Gateway's assigned address after deployment + // and patched onto the listener to enable cert-manager certificate issuance. var listenerName = tlsListenerIndex == 0 ? "https" : $"https-{tlsListenerIndex}"; tlsListenerIndex++; - var resolvedHost = await ResolveExpressionAsync(host, cancellationToken).ConfigureAwait(false); - var resolvedSecretName = await ResolveExpressionAsync(tls.SecretName, cancellationToken).ConfigureAwait(false); - gateway.Spec.Listeners.Add(new GatewayListenerV1 { Name = listenerName, Protocol = "HTTPS", Port = 443, - Hostname = resolvedHost, Tls = new GatewayTlsConfigV1 { Mode = "Terminate", @@ -744,6 +771,33 @@ private static async Task BuildGatewayObjects( } }); } + else + { + foreach (var host in tls.Hosts) + { + var listenerName = tlsListenerIndex == 0 ? "https" : $"https-{tlsListenerIndex}"; + tlsListenerIndex++; + + var resolvedHost = await ResolveExpressionAsync(host, cancellationToken).ConfigureAwait(false); + + gateway.Spec.Listeners.Add(new GatewayListenerV1 + { + Name = listenerName, + Protocol = "HTTPS", + Port = 443, + Hostname = resolvedHost, + Tls = new GatewayTlsConfigV1 + { + Mode = "Terminate", + CertificateRefs = { new GatewayCertificateRefV1 { Name = resolvedSecretName } } + }, + AllowedRoutes = new GatewayAllowedRoutesV1 + { + Namespaces = new GatewayRouteNamespacesV1 { From = "Same" } + } + }); + } + } } gatewayResource.GeneratedGateway = gateway; @@ -871,6 +925,526 @@ private static async Task BuildGatewayObjects( return tlsSecrets; } + /// + /// Collects gateway TLS configurations that have no hostnames specified and need + /// the assigned FQDN to be discovered from the Gateway's status after deployment. + /// + private static List<(KubernetesGatewayResource Gateway, ReferenceExpression SecretName)> CollectGatewaysNeedingFqdnDiscovery( + DistributedApplicationModel model, + KubernetesEnvironmentResource environment) + { + var results = new List<(KubernetesGatewayResource Gateway, ReferenceExpression SecretName)>(); + + foreach (var gateway in model.Resources.OfType().Where(g => g.Parent == environment)) + { + foreach (var tls in gateway.TlsConfigs) + { + if (tls.Hosts.Count == 0) + { + results.Add((gateway, tls.SecretName)); + } + } + } + + return results; + } + + /// + /// Discovers the assigned FQDN from the Gateway's status, patches the HTTPS listener + /// to include the hostname, and creates a bootstrap TLS secret so cert-manager can + /// issue a real certificate. + /// + private static async Task DiscoverFqdnAndBootstrapTlsAsync( + PipelineStepContext context, + KubernetesEnvironmentResource environment, + List<(KubernetesGatewayResource Gateway, ReferenceExpression SecretName)> gatewaysNeedingDiscovery) + { + var @namespace = "default"; + if (environment.TryGetLastAnnotation(out var nsAnnotation)) + { + var resolvedNs = await nsAnnotation.Namespace.GetValueAsync(context.CancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(resolvedNs)) + { + @namespace = resolvedNs; + } + } + + foreach (var (gateway, secretNameExpr) in gatewaysNeedingDiscovery) + { + var gatewayName = gateway.Name.ToKubernetesResourceName(); + var secretName = await ResolveExpressionAsync(secretNameExpr, context.CancellationToken).ConfigureAwait(false); + + // Poll for the Gateway's assigned hostname address. + // We use -o json and parse the full status to select Hostname-type addresses, + // since some controllers return IP addresses which are not valid for TLS hostnames. + context.Logger.LogInformation( + "Waiting for Gateway '{GatewayName}' to be assigned a hostname address...", gatewayName); + + var discoveredFqdn = await DiscoverGatewayFqdnAsync( + gatewayName, @namespace, environment, context).ConfigureAwait(false); + + if (string.IsNullOrEmpty(discoveredFqdn)) + { + context.Logger.LogWarning( + "Gateway '{GatewayName}' was not assigned a hostname address after waiting. " + + "TLS hostname discovery skipped. You may need to redeploy with an explicit hostname via WithHostname().", + gatewayName); + continue; + } + + context.Logger.LogInformation( + "Gateway '{GatewayName}' assigned address: {Fqdn}. Patching HTTPS listener(s) and bootstrapping TLS.", + gatewayName, discoveredFqdn); + + // Find HTTPS listeners without a hostname by parsing the full Gateway JSON. + var httpsListenerIndices = await FindHostnamelessHttpsListeners( + gatewayName, @namespace, environment, context).ConfigureAwait(false); + + if (httpsListenerIndices.Count == 0) + { + context.Logger.LogWarning( + "No HTTPS listeners without hostname found on Gateway '{GatewayName}'. Skipping hostname patch.", + gatewayName); + } + else + { + // Build the JSON patch using proper serialization to avoid injection issues. + var patchOperations = httpsListenerIndices.Select(idx => new + { + op = "add", + path = $"/spec/listeners/{idx}/hostname", + value = discoveredFqdn + }); + var patchJson = System.Text.Json.JsonSerializer.Serialize(patchOperations); + + // Write patch to a temp file to avoid shell escaping issues with kubectl -p + var patchTempDir = Directory.CreateTempSubdirectory(".aspire-gateway-patch"); + try + { + var patchFilePath = Path.Combine(patchTempDir.FullName, "patch.json"); + await File.WriteAllTextAsync(patchFilePath, patchJson, context.CancellationToken).ConfigureAwait(false); + + var patchArgs = $"patch gateway {gatewayName} --namespace {@namespace} --type=json --patch-file \"{patchFilePath}\""; + if (environment.KubeConfigPath is not null) + { + patchArgs += $" --kubeconfig \"{environment.KubeConfigPath}\""; + } + + var (patchResult, patchDisposable) = ProcessUtil.Run(new ProcessSpec("kubectl") + { + Arguments = patchArgs, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true, + OnOutputData = line => context.Logger.LogDebug("{Line}", line), + OnErrorData = line => context.Logger.LogDebug("{Line}", line) + }); + + await using (patchDisposable.ConfigureAwait(false)) + { + var patchExitResult = await patchResult.WaitAsync(context.CancellationToken).ConfigureAwait(false); + if (patchExitResult.ExitCode != 0) + { + context.Logger.LogWarning( + "Failed to patch Gateway '{GatewayName}' with hostname '{Hostname}' (exit code {ExitCode}). " + + "You may need to redeploy with an explicit hostname via WithHostname().", + gatewayName, discoveredFqdn, patchExitResult.ExitCode); + continue; + } + } + } + finally + { + try { patchTempDir.Delete(recursive: true); } catch { } + } + + // Transfer field ownership to Helm using server-side apply with a minimal + // Gateway manifest so subsequent Helm deploys don't encounter SSA conflicts. + // We construct a minimal spec rather than re-applying kubectl get output, + // which would include server-populated fields (status, resourceVersion, etc.). + await TransferGatewayFieldOwnership( + gatewayName, @namespace, environment, context).ConfigureAwait(false); + } + + // Check if bootstrap TLS secret already exists + var checkArgs = $"get secret {secretName} --namespace {@namespace}"; + if (environment.KubeConfigPath is not null) + { + checkArgs += $" --kubeconfig \"{environment.KubeConfigPath}\""; + } + + var (checkResult, checkDisposable) = ProcessUtil.Run(new ProcessSpec("kubectl") + { + Arguments = checkArgs, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true, + OnOutputData = _ => { }, + OnErrorData = _ => { } + }); + + await using (checkDisposable.ConfigureAwait(false)) + { + var result = await checkResult.WaitAsync(context.CancellationToken).ConfigureAwait(false); + if (result.ExitCode == 0) + { + context.Logger.LogInformation("TLS secret '{SecretName}' already exists, skipping bootstrap.", secretName); + continue; + } + } + + // Create a bootstrap self-signed cert with the discovered FQDN + context.Logger.LogInformation("Creating bootstrap TLS secret '{SecretName}' for '{Hostname}'.", secretName, discoveredFqdn); + + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var certRequest = new CertificateRequest($"CN={discoveredFqdn}", ecdsa, HashAlgorithmName.SHA256); + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName(discoveredFqdn); + certRequest.CertificateExtensions.Add(sanBuilder.Build()); + using var cert = certRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1)); + + var certPem = cert.ExportCertificatePem(); + var keyPem = ecdsa.ExportECPrivateKeyPem(); + + var tempDir = Directory.CreateTempSubdirectory(".aspire-tls-discovery"); + try + { + var certPath = Path.Combine(tempDir.FullName, "tls.crt"); + var keyPath = Path.Combine(tempDir.FullName, "tls.key"); + await File.WriteAllTextAsync(certPath, certPem, context.CancellationToken).ConfigureAwait(false); + await File.WriteAllTextAsync(keyPath, keyPem, context.CancellationToken).ConfigureAwait(false); + + var createArgs = $"create secret tls {secretName} --cert=\"{certPath}\" --key=\"{keyPath}\" --namespace {@namespace}"; + if (environment.KubeConfigPath is not null) + { + createArgs += $" --kubeconfig \"{environment.KubeConfigPath}\""; + } + + var (createResult, createDisposable) = ProcessUtil.Run(new ProcessSpec("kubectl") + { + Arguments = createArgs, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true, + OnOutputData = line => context.Logger.LogDebug("{Line}", line), + OnErrorData = line => context.Logger.LogDebug("{Line}", line) + }); + + await using (createDisposable.ConfigureAwait(false)) + { + var createExitResult = await createResult.WaitAsync(context.CancellationToken).ConfigureAwait(false); + if (createExitResult.ExitCode != 0) + { + context.Logger.LogWarning("Failed to create bootstrap TLS secret '{SecretName}' (exit code {ExitCode}).", secretName, createExitResult.ExitCode); + } + else + { + context.Logger.LogInformation( + "Bootstrap TLS secret '{SecretName}' created for '{Hostname}'. " + + "cert-manager will replace this with a real certificate once the hostname is detected on the Gateway listener.", + secretName, discoveredFqdn); + } + } + } + finally + { + try { tempDir.Delete(recursive: true); } catch { } + } + } + } + + /// + /// Polls for the Gateway's assigned hostname address using a Polly retry pipeline. + /// Retries up to 60 times with 5-second delays (5 minutes total). + /// + private static async Task DiscoverGatewayFqdnAsync( + string gatewayName, + string @namespace, + KubernetesEnvironmentResource environment, + PipelineStepContext context) + { + var pipeline = new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = 59, + Delay = TimeSpan.FromSeconds(5), + BackoffType = DelayBackoffType.Constant, + ShouldHandle = new PredicateBuilder().HandleResult(r => r is null), + OnRetry = args => + { + context.Logger.LogDebug( + "Gateway '{GatewayName}' address not yet available (attempt {Attempt}).", + gatewayName, args.AttemptNumber + 1); + return default; + } + }) + .Build(); + + return await pipeline.ExecuteAsync(async ct => + { + var getArgs = $"get gateway {gatewayName} --namespace {@namespace} -o json"; + if (environment.KubeConfigPath is not null) + { + getArgs += $" --kubeconfig \"{environment.KubeConfigPath}\""; + } + + var stdout = new List(); + var (getResult, getDisposable) = ProcessUtil.Run(new ProcessSpec("kubectl") + { + Arguments = getArgs, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true, + OnOutputData = stdout.Add, + OnErrorData = _ => { } + }); + + await using (getDisposable.ConfigureAwait(false)) + { + var result = await getResult.WaitAsync(ct).ConfigureAwait(false); + if (result.ExitCode == 0 && stdout.Count > 0) + { + return ExtractHostnameFromGatewayJson(string.Join("", stdout)); + } + } + + return null; + }, context.CancellationToken).ConfigureAwait(false); + } + + /// + /// Extracts the first Hostname-type address from Gateway JSON status. + /// Returns null if no hostname address is found (e.g., only IP addresses). + /// + private static string? ExtractHostnameFromGatewayJson(string gatewayJson) + { + try + { + using var doc = System.Text.Json.JsonDocument.Parse(gatewayJson); + if (doc.RootElement.TryGetProperty("status", out var status) && + status.TryGetProperty("addresses", out var addresses)) + { + // Prefer Hostname-type addresses over IPAddress + foreach (var addr in addresses.EnumerateArray()) + { + if (addr.TryGetProperty("type", out var type) && + string.Equals(type.GetString(), "Hostname", StringComparison.OrdinalIgnoreCase) && + addr.TryGetProperty("value", out var value)) + { + var hostname = value.GetString()?.Trim(); + if (!string.IsNullOrEmpty(hostname)) + { + return hostname; + } + } + } + + // Fall back to any address that looks like a DNS name (contains a dot, no colons) + foreach (var addr in addresses.EnumerateArray()) + { + if (addr.TryGetProperty("value", out var value)) + { + var addrValue = value.GetString()?.Trim(); + if (!string.IsNullOrEmpty(addrValue) && addrValue.Contains('.') && !addrValue.Contains(':')) + { + return addrValue; + } + } + } + } + } + catch (System.Text.Json.JsonException) + { + // Gateway JSON was malformed + } + + return null; + } + + /// + /// Finds HTTPS listener indices that don't have a hostname set, by parsing the + /// full Gateway JSON from kubectl. + /// + private static async Task> FindHostnamelessHttpsListeners( + string gatewayName, + string @namespace, + KubernetesEnvironmentResource environment, + PipelineStepContext context) + { + var getArgs = $"get gateway {gatewayName} --namespace {@namespace} -o json"; + if (environment.KubeConfigPath is not null) + { + getArgs += $" --kubeconfig \"{environment.KubeConfigPath}\""; + } + + var stdout = new List(); + var (getResult, getDisposable) = ProcessUtil.Run(new ProcessSpec("kubectl") + { + Arguments = getArgs, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true, + OnOutputData = stdout.Add, + OnErrorData = _ => { } + }); + + var indices = new List(); + await using (getDisposable.ConfigureAwait(false)) + { + var result = await getResult.WaitAsync(context.CancellationToken).ConfigureAwait(false); + if (result.ExitCode == 0 && stdout.Count > 0) + { + try + { + using var doc = System.Text.Json.JsonDocument.Parse(string.Join("", stdout)); + if (doc.RootElement.TryGetProperty("spec", out var spec) && + spec.TryGetProperty("listeners", out var listeners)) + { + for (var i = 0; i < listeners.GetArrayLength(); i++) + { + var listener = listeners[i]; + if (listener.TryGetProperty("protocol", out var protocol) && + string.Equals(protocol.GetString(), "HTTPS", StringComparison.OrdinalIgnoreCase) && + !listener.TryGetProperty("hostname", out _)) + { + indices.Add(i); + } + } + } + } + catch (System.Text.Json.JsonException) + { + // Fall back to assuming index 1 (HTTP=0, HTTPS=1) + indices.Add(1); + } + } + else + { + // Fall back to assuming index 1 + indices.Add(1); + } + } + + return indices; + } + + /// + /// Transfers field ownership of the Gateway's patched hostname to Helm using server-side apply + /// with a minimal Gateway manifest, avoiding server-populated fields like status and resourceVersion. + /// + private static async Task TransferGatewayFieldOwnership( + string gatewayName, + string @namespace, + KubernetesEnvironmentResource environment, + PipelineStepContext context) + { + // Build a minimal Gateway JSON with only the fields needed for ownership transfer. + // We read the current Gateway, strip server-populated fields, update the hostname, + // and re-apply with Helm's field manager. + var getArgs = $"get gateway {gatewayName} --namespace {@namespace} -o json"; + if (environment.KubeConfigPath is not null) + { + getArgs += $" --kubeconfig \"{environment.KubeConfigPath}\""; + } + + var stdout = new List(); + var (getResult, getDisposable) = ProcessUtil.Run(new ProcessSpec("kubectl") + { + Arguments = getArgs, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true, + OnOutputData = stdout.Add, + OnErrorData = _ => { } + }); + + await using (getDisposable.ConfigureAwait(false)) + { + var exitResult = await getResult.WaitAsync(context.CancellationToken).ConfigureAwait(false); + if (exitResult.ExitCode != 0 || stdout.Count == 0) + { + context.Logger.LogDebug("Could not read Gateway for field ownership transfer."); + return; + } + } + + try + { + using var doc = System.Text.Json.JsonDocument.Parse(string.Join("", stdout)); + var root = doc.RootElement; + + // Build a minimal manifest: apiVersion, kind, metadata (name, namespace, annotations, labels), spec + var metadataNode = new System.Text.Json.Nodes.JsonObject + { + ["name"] = gatewayName, + ["namespace"] = @namespace + }; + + // Preserve annotations and labels from the current Gateway + if (root.TryGetProperty("metadata", out var metadata)) + { + if (metadata.TryGetProperty("annotations", out var annotations)) + { + metadataNode["annotations"] = System.Text.Json.Nodes.JsonNode.Parse(annotations.GetRawText()); + } + + if (metadata.TryGetProperty("labels", out var labels)) + { + metadataNode["labels"] = System.Text.Json.Nodes.JsonNode.Parse(labels.GetRawText()); + } + } + + var minimal = new System.Text.Json.Nodes.JsonObject + { + ["apiVersion"] = root.GetProperty("apiVersion").GetString(), + ["kind"] = root.GetProperty("kind").GetString(), + ["metadata"] = metadataNode + }; + + // Copy the spec as-is (it already has the patched hostname from the previous step) + if (root.TryGetProperty("spec", out var spec)) + { + minimal["spec"] = System.Text.Json.Nodes.JsonNode.Parse(spec.GetRawText()); + } + + var tempDir = Directory.CreateTempSubdirectory(".aspire-gateway-ownership"); + try + { + var manifestPath = Path.Combine(tempDir.FullName, "gateway.json"); + await File.WriteAllTextAsync(manifestPath, minimal.ToJsonString(), context.CancellationToken).ConfigureAwait(false); + + var applyArgs = $"apply --server-side --field-manager=helm --force-conflicts -f \"{manifestPath}\""; + if (environment.KubeConfigPath is not null) + { + applyArgs += $" --kubeconfig \"{environment.KubeConfigPath}\""; + } + + var (applyResult, applyDisposable) = ProcessUtil.Run(new ProcessSpec("kubectl") + { + Arguments = applyArgs, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true, + OnOutputData = line => context.Logger.LogDebug("{Line}", line), + OnErrorData = line => context.Logger.LogDebug("{Line}", line) + }); + + await using (applyDisposable.ConfigureAwait(false)) + { + var applyExitResult = await applyResult.WaitAsync(context.CancellationToken).ConfigureAwait(false); + if (applyExitResult.ExitCode != 0) + { + context.Logger.LogDebug( + "Failed to transfer field ownership to Helm (exit code {ExitCode}). " + + "Subsequent deploys with an explicit hostname may require --force.", + applyExitResult.ExitCode); + } + } + } + finally + { + try { tempDir.Delete(recursive: true); } catch { } + } + } + catch (System.Text.Json.JsonException ex) + { + context.Logger.LogDebug(ex, "Failed to parse Gateway JSON for field ownership transfer."); + } + } + private static async Task BootstrapTlsSecretsAsync( PipelineStepContext context, KubernetesEnvironmentResource environment, @@ -920,6 +1494,9 @@ private static async Task BootstrapTlsSecretsAsync( using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); var request = new CertificateRequest($"CN={hostname}", ecdsa, HashAlgorithmName.SHA256); + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName(hostname); + request.CertificateExtensions.Add(sanBuilder.Build()); using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1)); var certPem = cert.ExportCertificatePem(); diff --git a/tests/Aspire.Deployment.EndToEnd.Tests/KubernetesGatewayTlsDeploymentTests.cs b/tests/Aspire.Deployment.EndToEnd.Tests/KubernetesGatewayTlsDeploymentTests.cs new file mode 100644 index 00000000000..652c5f0f2aa --- /dev/null +++ b/tests/Aspire.Deployment.EndToEnd.Tests/KubernetesGatewayTlsDeploymentTests.cs @@ -0,0 +1,504 @@ +// 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.Tests.Utils; +using Aspire.Deployment.EndToEnd.Tests.Helpers; +using Hex1b.Automation; +using Xunit; + +namespace Aspire.Deployment.EndToEnd.Tests; + +/// +/// End-to-end test for deploying an Aspire application to a pre-created AKS cluster +/// with Kubernetes Gateway API + TLS using AddKubernetesEnvironment and AddGateway. +/// The test creates the AKS cluster with ALB controller and Gateway API support, installs +/// cert-manager with a Let's Encrypt HTTP-01 ClusterIssuer (gatewayHTTPRoute solver), then +/// uses aspire deploy with WithTls() (no hostname). It verifies that: +/// 1. The Gateway gets an FQDN assigned by AGC +/// 2. The FQDN discovery pipeline step patches the hostname onto the HTTPS listener +/// 3. cert-manager issues a real TLS certificate via HTTP-01 +/// 4. The app is accessible via port-forward and over HTTPS +/// +public sealed class KubernetesGatewayTlsDeploymentTests(ITestOutputHelper output) +{ + private static readonly TimeSpan s_testTimeout = TimeSpan.FromMinutes(60); + + [Fact] + public async Task DeployStarterWithGatewayTlsToKubernetes() + { + using var cts = new CancellationTokenSource(s_testTimeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( + cts.Token, TestContext.Current.CancellationToken); + var cancellationToken = linkedCts.Token; + + await DeployStarterWithGatewayTlsToKubernetesCore(cancellationToken); + } + + private async Task DeployStarterWithGatewayTlsToKubernetesCore(CancellationToken cancellationToken) + { + var subscriptionId = AzureAuthenticationHelpers.TryGetSubscriptionId(); + if (string.IsNullOrEmpty(subscriptionId)) + { + Assert.Skip("Azure subscription not configured. Set ASPIRE_DEPLOYMENT_TEST_SUBSCRIPTION."); + } + + if (!AzureAuthenticationHelpers.IsAzureAuthAvailable()) + { + if (DeploymentE2ETestHelpers.IsRunningInCI) + { + Assert.Fail("Azure authentication not available in CI. Check OIDC configuration."); + } + else + { + Assert.Skip("Azure authentication not available. Run 'az login' to authenticate."); + } + } + + var workspace = TemporaryWorkspace.Create(output); + var startTime = DateTime.UtcNow; + + var resourceGroupName = DeploymentE2ETestHelpers.GenerateResourceGroupName("k8sgwtls"); + var clusterName = $"aks-{DeploymentE2ETestHelpers.GetRunId()}-{DeploymentE2ETestHelpers.GetRunAttempt()}"; + var acrName = $"acrgw{DeploymentE2ETestHelpers.GetRunId()}{DeploymentE2ETestHelpers.GetRunAttempt()}".ToLowerInvariant(); + acrName = new string(acrName.Where(char.IsLetterOrDigit).Take(50).ToArray()); + if (acrName.Length < 5) + { + acrName = $"acrtest{Guid.NewGuid():N}"[..24]; + } + + var projectName = "K8sGatewayTls"; + var k8sNamespace = "gwtls"; + + output.WriteLine($"Test: {nameof(DeployStarterWithGatewayTlsToKubernetes)}"); + output.WriteLine($"Project Name: {projectName}"); + output.WriteLine($"Resource Group: {resourceGroupName}"); + output.WriteLine($"AKS Cluster: {clusterName}"); + output.WriteLine($"ACR Name: {acrName}"); + output.WriteLine($"K8s Namespace: {k8sNamespace}"); + output.WriteLine($"Subscription: {subscriptionId[..8]}..."); + output.WriteLine($"Workspace: {workspace.WorkspaceRoot.FullName}"); + + try + { + using var terminal = DeploymentE2ETestHelpers.CreateTestTerminal(); + var pendingRun = terminal.RunAsync(cancellationToken); + + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + + // ===== PHASE 1: Provision AKS with ALB + cert-manager ===== + + output.WriteLine("Step 1: Preparing environment..."); + await auto.PrepareEnvironmentAsync(workspace, counter); + + // Register resource providers for AGC + Gateway API + output.WriteLine("Step 2: Registering resource providers..."); + await auto.TypeAsync( + "az provider register --namespace Microsoft.ContainerService --wait && " + + "az provider register --namespace Microsoft.ContainerRegistry --wait && " + + "az provider register --namespace Microsoft.Network --wait && " + + "az provider register --namespace Microsoft.NetworkFunction --wait && " + + "az provider register --namespace Microsoft.ServiceNetworking --wait"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(5)); + + await auto.TypeAsync( + "az feature register --namespace Microsoft.ContainerService --name ManagedGatewayAPIPreview 2>/dev/null || true && " + + "az feature register --namespace Microsoft.ContainerService --name ApplicationLoadBalancerPreview 2>/dev/null || true && " + + "az provider register --namespace Microsoft.ContainerService"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); + + await auto.TypeAsync("az extension add --name alb --yes 2>/dev/null || true && az extension add --name aks-preview --yes 2>/dev/null || true"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + // Create resource group + output.WriteLine("Step 3: Creating resource group..."); + await auto.TypeAsync($"az group create --name {resourceGroupName} --location westus3 --output table"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + // Create ACR + output.WriteLine("Step 4: Creating ACR..."); + await auto.TypeAsync($"az acr create --resource-group {resourceGroupName} --name {acrName} --sku Basic --output table"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(3)); + + // Login to ACR early (OIDC token expires during AKS creation) + await auto.TypeAsync($"az acr login --name {acrName}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + // Create AKS with Azure CNI, OIDC, workload identity, Gateway API, and ALB controller + // Per https://learn.microsoft.com/azure/application-gateway/for-containers/quickstart-deploy-application-gateway-for-containers-alb-controller-addon + output.WriteLine("Step 5: Creating AKS cluster with Gateway API + ALB (10-15 minutes)..."); + await auto.TypeAsync( + $"az aks create " + + $"--resource-group {resourceGroupName} " + + $"--name {clusterName} " + + $"--location westus3 " + + $"--node-count 1 " + + $"--node-vm-size Standard_D2as_v5 " + + $"--network-plugin azure " + + $"--generate-ssh-keys " + + $"--attach-acr {acrName} " + + $"--enable-oidc-issuer " + + $"--enable-workload-identity " + + $"--enable-gateway-api " + + $"--enable-application-load-balancer " + + $"--output table"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(20)); + + // Get credentials + await auto.TypeAsync($"az aks get-credentials --resource-group {resourceGroupName} --name {clusterName} --overwrite-existing"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); + + // Verify ALB controller is running and GatewayClass exists + // The ALB controller pods may still be initializing after cluster creation, + // so poll until they are running and the GatewayClass is available. + output.WriteLine("Step 6: Waiting for ALB controller and GatewayClass..."); + await auto.TypeAsync( + "for i in $(seq 1 60); do " + + "READY=$(kubectl get pods -n kube-system -l app=alb-controller -o jsonpath='{.items[0].status.phase}' 2>/dev/null); " + + "[ \"$READY\" = \"Running\" ] && kubectl get gatewayclass azure-alb-external >/dev/null 2>&1 && " + + "echo 'ALB controller running and GatewayClass available' && break; " + + "echo \"Attempt $i: ALB controller status=$READY, waiting...\"; sleep 10; done && " + + "kubectl get pods -n kube-system | grep alb-controller && " + + "kubectl get gatewayclass azure-alb-external"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(10)); + + // Create ApplicationLoadBalancer CRD using the add-on's auto-created subnet + output.WriteLine("Step 7: Creating ApplicationLoadBalancer..."); + await auto.TypeAsync( + $"MC_RG=$(az aks show -g {resourceGroupName} -n {clusterName} --query nodeResourceGroup -o tsv) && " + + "SUBNET_ID=$(az network vnet subnet show -g $MC_RG " + + "--vnet-name $(az network vnet list -g $MC_RG --query '[0].name' -o tsv) " + + "--name aks-appgateway --query id -o tsv) && " + + "echo \"Subnet: $SUBNET_ID\""); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + await auto.TypeAsync( + "cat </dev/null); " + + "[ \"$STATUS\" = \"True\" ] && echo 'ALB Ready' && break; sleep 5; done"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(6)); + + // Install cert-manager with Gateway API support + output.WriteLine("Step 8: Installing cert-manager..."); + await auto.TypeAsync( + "helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager " + + "--namespace cert-manager --create-namespace " + + "--set crds.enabled=true --set config.enableGatewayAPI=true --wait"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(5)); + + // Create HTTP-01 ClusterIssuer + output.WriteLine("Step 9: Creating HTTP-01 ClusterIssuer..."); + await auto.TypeAsync( + "cat < + { + helm.WithNamespace(builder.AddParameter("namespace")); + helm.WithChartVersion(builder.AddParameter("chartversion")); + }); + +var gateway = k8s.AddGateway("ingress") + .WithGatewayClass("azure-alb-external") + .WithGatewayAnnotation("alb.networking.azure.io/alb-name", "alb-aspire") + .WithGatewayAnnotation("alb.networking.azure.io/alb-namespace", "default") + .WithGatewayAnnotation("cert-manager.io/cluster-issuer", "letsencrypt-http01") + .WithRoute("/", webfrontend.GetEndpoint("http")) + .WithTls(); + +builder.Build().Run(); +"""; + + content = content.Replace(buildRunPattern, replacement); + + // Add required pragmas and using directive at the top of the file + var topOfFile = "#pragma warning disable ASPIREPIPELINES001\n#pragma warning disable ASPIRECOMPUTE003\nusing Aspire.Hosting.Kubernetes;\n"; + if (!content.Contains("#pragma warning disable ASPIREPIPELINES001")) + { + content = topOfFile + content; + } + + File.WriteAllText(appHostFilePath, content); + output.WriteLine("Modified AppHost.cs with AddKubernetesEnvironment + AddGateway + WithTls"); + + // Navigate to AppHost dir + await auto.TypeAsync($"cd {projectName}.AppHost"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // ===== PHASE 3: Deploy with aspire deploy ===== + + // Refresh ACR login + output.WriteLine("Step 9: Refreshing ACR login..."); + await auto.TypeAsync($"az acr login --name {acrName}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + // Set parameters as environment variables so aspire deploy doesn't prompt + output.WriteLine("Step 9: Setting deployment parameters..."); + await auto.TypeAsync( + $"export Parameters__registryendpoint={acrName}.azurecr.io && " + + $"export Parameters__namespace={k8sNamespace} && " + + "export Parameters__chartversion=0.1.0"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Deploy using aspire deploy + output.WriteLine("Step 9: Running aspire deploy..."); + await auto.TypeAsync("aspire deploy"); + await auto.EnterAsync(); + await auto.WaitForPipelineSuccessAsync(timeout: TimeSpan.FromMinutes(15)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); + + // ===== PHASE 4: Verify Gateway TLS ===== + + // Wait for pods + output.WriteLine("Step 9: Waiting for pods..."); + await auto.TypeAsync($"kubectl wait --for=condition=Ready pod --all -n {k8sNamespace} --timeout=300s"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(6)); + + await auto.TypeAsync($"kubectl get pods -n {k8sNamespace}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Check Gateway has address + output.WriteLine("Step 9: Checking Gateway address..."); + await auto.TypeAsync( + $"for i in $(seq 1 30); do " + + $"FQDN=$(kubectl get gateway ingress -n {k8sNamespace} -o jsonpath='{{.status.addresses[0].value}}' 2>/dev/null); " + + "[ -n \"$FQDN\" ] && echo \"Gateway FQDN: $FQDN\" && break; sleep 5; done"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(3)); + + // Check HTTPS listener has hostname (patched by FQDN discovery step) + output.WriteLine("Step 9: Checking HTTPS listener hostname..."); + await auto.TypeAsync( + $"kubectl get gateway ingress -n {k8sNamespace} " + + "-o jsonpath='{range .spec.listeners[*]}{.name} {.protocol} {.hostname}{\"\\n\"}{end}'"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); + + // Wait for certificate + output.WriteLine("Step 9: Waiting for TLS certificate (up to 10 minutes)..."); + await auto.TypeAsync( + $"for i in $(seq 1 60); do " + + $"READY=$(kubectl get certificate -n {k8sNamespace} -o jsonpath='{{.items[0].status.conditions[?(@.type==\"Ready\")].status}}' 2>/dev/null); " + + "[ \"$READY\" = \"True\" ] && echo 'Certificate Ready!' && break; " + + "echo \"Attempt $i: waiting...\"; sleep 10; done && " + + $"kubectl get certificate -n {k8sNamespace}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(12)); + + // Test HTTPS access + output.WriteLine("Step 9: Testing HTTPS access..."); + await auto.TypeAsync( + $"FQDN=$(kubectl get gateway ingress -n {k8sNamespace} -o jsonpath='{{.status.addresses[0].value}}') && " + + "echo \"Testing: https://$FQDN\" && " + + "OK=0; for i in $(seq 1 10); do sleep 5; " + + "S=$(curl -so /dev/null -w '%{http_code}' -m 10 https://$FQDN/ 2>/dev/null); " + + "[ \"$S\" = \"200\" ] && echo \"HTTPS $S OK\" && OK=1 && break; " + + "echo \"Attempt $i: $S\"; done; " + + "[ \"$OK\" = \"1\" ] || echo 'WARN: HTTPS not 200 yet (cert may still be provisioning)'"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); + + // Verify via port-forward (confirms app is running regardless of external TLS) + output.WriteLine("Step 9: Verifying app via port-forward..."); + await auto.TypeAsync($"kubectl port-forward svc/webfrontend-service 18081:8080 -n {k8sNamespace} &"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(10)); + + await auto.TypeAsync("sleep 3"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(10)); + + await auto.TypeAsync( + "OK=0; for i in $(seq 1 10); do sleep 3 && " + + "curl -sf http://localhost:18081/ -o /dev/null -w '%{http_code}' && " + + "echo ' OK' && OK=1 && break; done; " + + "[ \"$OK\" = \"1\" ] || { echo 'FAIL: webfrontend unreachable'; exit 1; }"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + await auto.TypeAsync("kill %1 2>/dev/null; true"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(10)); + + // ===== PHASE 5: Cleanup ===== + + output.WriteLine("Step 9: Destroying deployment..."); + await auto.AspireDestroyAsync(counter); + + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + + await pendingRun; + + var duration = DateTime.UtcNow - startTime; + output.WriteLine($"Gateway TLS deployment completed in {duration}"); + + DeploymentReporter.ReportDeploymentSuccess( + nameof(DeployStarterWithGatewayTlsToKubernetes), + resourceGroupName, + new Dictionary + { + ["cluster"] = clusterName, + ["acr"] = acrName, + ["project"] = projectName + }, + duration); + + output.WriteLine("✅ Test passed - Aspire app deployed with Gateway API TLS via HTTP-01!"); + } + catch (Exception ex) + { + var duration = DateTime.UtcNow - startTime; + output.WriteLine($"❌ Test failed after {duration}: {ex.Message}"); + + DeploymentReporter.ReportDeploymentFailure( + nameof(DeployStarterWithGatewayTlsToKubernetes), + resourceGroupName, + ex.Message, + ex.StackTrace); + + throw; + } + finally + { + output.WriteLine($"Cleaning up resource group: {resourceGroupName}"); + await CleanupResourceGroupAsync(resourceGroupName); + } + } + + private async Task CleanupResourceGroupAsync(string resourceGroupName) + { + try + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "az", + Arguments = $"group delete --name {resourceGroupName} --yes --no-wait", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + } + }; + + process.Start(); + await process.WaitForExitAsync(); + + if (process.ExitCode == 0) + { + output.WriteLine($"Resource group deletion initiated: {resourceGroupName}"); + DeploymentReporter.ReportCleanupStatus(resourceGroupName, success: true, "Deletion initiated"); + } + else + { + var error = await process.StandardError.ReadToEndAsync(); + output.WriteLine($"Resource group deletion may have failed (exit code {process.ExitCode}): {error}"); + DeploymentReporter.ReportCleanupStatus(resourceGroupName, success: false, $"Exit code {process.ExitCode}: {error}"); + } + } + catch (Exception ex) + { + output.WriteLine($"Failed to cleanup resource group: {ex.Message}"); + DeploymentReporter.ReportCleanupStatus(resourceGroupName, success: false, ex.Message); + } + } +} diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs b/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs index 88073a59ac7..bc3186aed79 100644 --- a/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs +++ b/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs @@ -227,4 +227,46 @@ public async Task AddGateway_BackwardCompatible_NoGatewayNoChange() Assert.DoesNotContain(files, f => f.Contains("gateway", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(files, f => f.Contains("route", StringComparison.OrdinalIgnoreCase)); } + + [Fact] + public async Task AddGateway_WithTls_NoHostname_GeneratesHttpsListenerWithoutHostname() + { + using var tempDir = new TestTempDirectory(); + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path); + + var k8s = builder.AddKubernetesEnvironment("env"); + var gateway = k8s.AddGateway("public").WithGatewayClass("azure-alb-external"); + + var api = builder.AddContainer("myapi", "nginx") + .WithHttpEndpoint(targetPort: 8080); + + // WithTls() without WithHostname() — should still generate an HTTPS listener + gateway + .WithRoute("/", api.GetEndpoint("http")) + .WithTls("my-tls-secret"); + + var app = builder.Build(); + app.Run(); + + // Check Gateway has HTTPS listener without a hostname + var gatewayFile = Path.Combine(tempDir.Path, "templates", "public", "public.yaml"); + var content = await File.ReadAllTextAsync(gatewayFile); + + Assert.Contains("HTTPS", content); + Assert.Contains("Terminate", content); + Assert.Contains("my-tls-secret", content); + // Should also have HTTP listener + Assert.Contains("HTTP", content); + + // The HTTPS listener should NOT have a hostname field (since no WithHostname was called) + // Verify it has the listener but the hostname line should not appear after HTTPS + var lines = content.Split('\n').Select(l => l.Trim()).ToList(); + var httpsIndex = lines.FindIndex(l => l.Contains("protocol:") && l.Contains("HTTPS")); + Assert.True(httpsIndex >= 0, "HTTPS listener not found in:\n" + content); + + // Find the next listener or end of listeners to check there's no hostname + var nextListenerOrEnd = lines.FindIndex(httpsIndex + 1, l => l.StartsWith("- name:") || l == ""); + var httpsSection = lines.Skip(httpsIndex).Take((nextListenerOrEnd > httpsIndex ? nextListenerOrEnd : lines.Count) - httpsIndex); + Assert.DoesNotContain(httpsSection, l => l.StartsWith("hostname:") || l.StartsWith("hostname ")); + } } From 34a86ad1a0687c28709d080afbe135d372e5f6fd Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 14:39:34 -0700 Subject: [PATCH 18/55] [release/13.3] Fix Python starter TypeScript health check build (#16647) * Fix Python starter health check template Update the Python starter TypeScript AppHost to use the supported withHttpHealthCheck options object and align its root build script with AppHost-only type checking. Add E2E coverage that verifies the generated starter builds successfully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use fail-fast helper for Python starter build test Update the Python React template E2E build verification to use the existing fail-fast command helper so npm build failures are surfaced immediately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Verify TypeScript templates build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add build script aliases to TypeScript AppHost scaffold Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use AppHost tsconfig for TypeScript AppHost lint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use AppHost tsconfig name for Python starter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Sebastien Ros Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Templating/Templates/py-starter/apphost.ts | 2 +- .../Templates/py-starter/eslint.config.mjs | 3 ++- .../Templating/Templates/py-starter/package.json | 16 ++++++++++------ .../Templates/py-starter/tsconfig.apphost.json | 15 +++++++++++++++ .../Templates/py-starter/tsconfig.json | 8 -------- .../Templates/ts-starter/eslint.config.mjs | 3 ++- .../TypeScriptLanguageSupport.cs | 16 ++++++++++++++-- .../PythonReactTemplateTests.cs | 5 ++++- .../TypeScriptEmptyAppHostTemplateTests.cs | 2 ++ .../TypeScriptStarterTemplateTests.cs | 2 ++ .../TypeScriptLanguageSupportTests.cs | 10 +++++++--- 11 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.apphost.json delete mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts b/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts index e51954bafab..d3c55007e81 100644 --- a/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts +++ b/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts @@ -17,7 +17,7 @@ const app = await builder .withReference(cache) .waitFor(cache) // {{/redis}} - .withHttpHealthCheck("/health"); + .withHttpHealthCheck({ path: "/health" }); // Run the Vite frontend after the API and inject the API URL for local proxying. const frontend = await builder diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs b/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs index e7e33edb3fd..2c6536d4bd5 100644 --- a/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs +++ b/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs @@ -8,7 +8,8 @@ export default defineConfig({ extends: [tseslint.configs.base], languageOptions: { parserOptions: { - projectService: true, + project: './tsconfig.apphost.json', + tsconfigRootDir: import.meta.dirname, }, }, rules: { diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/package.json b/src/Aspire.Cli/Templating/Templates/py-starter/package.json index 7878cd35dbe..c64a43c21c9 100644 --- a/src/Aspire.Cli/Templating/Templates/py-starter/package.json +++ b/src/Aspire.Cli/Templating/Templates/py-starter/package.json @@ -3,12 +3,16 @@ "private": true, "type": "module", "scripts": { - "lint": "eslint apphost.ts", - "predev": "npm run lint", - "dev": "aspire run", - "prebuild": "npm run lint", - "build": "tsc", - "watch": "tsc --watch" + "aspire:lint": "eslint apphost.ts", + "aspire:start": "aspire run", + "aspire:build": "tsc -p tsconfig.apphost.json", + "aspire:dev": "tsc --watch -p tsconfig.apphost.json", + "lint": "npm run aspire:lint", + "predev": "npm run aspire:lint", + "dev": "npm run aspire:start", + "prebuild": "npm run aspire:lint", + "build": "npm run aspire:build", + "watch": "npm run aspire:dev" }, "dependencies": { "vscode-jsonrpc": "^8.2.0" diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.apphost.json b/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.apphost.json new file mode 100644 index 00000000000..939a631a123 --- /dev/null +++ b/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.apphost.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "./dist/apphost", + "rootDir": ".", + "strict": true + }, + "include": ["apphost.ts", ".modules/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json b/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json deleted file mode 100644 index 1e44664428b..00000000000 --- a/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "module": "esnext", - "moduleResolution": "bundler", - "strict": true - } -} diff --git a/src/Aspire.Cli/Templating/Templates/ts-starter/eslint.config.mjs b/src/Aspire.Cli/Templating/Templates/ts-starter/eslint.config.mjs index e7e33edb3fd..2c6536d4bd5 100644 --- a/src/Aspire.Cli/Templating/Templates/ts-starter/eslint.config.mjs +++ b/src/Aspire.Cli/Templating/Templates/ts-starter/eslint.config.mjs @@ -8,7 +8,8 @@ export default defineConfig({ extends: [tseslint.configs.base], languageOptions: { parserOptions: { - projectService: true, + project: './tsconfig.apphost.json', + tsconfigRootDir: import.meta.dirname, }, }, rules: { diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs index 466b9aa1738..9fb69b3952d 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs @@ -96,7 +96,8 @@ public Dictionary Scaffold(ScaffoldRequest request) extends: [tseslint.configs.base], languageOptions: { parserOptions: { - projectService: true, + project: './tsconfig.apphost.json', + tsconfigRootDir: import.meta.dirname, }, }, rules: { @@ -142,7 +143,8 @@ private static string CreatePackageJson(ScaffoldRequest request) var packageJson = new JsonObject(); var packageJsonPath = Path.Combine(request.TargetPath, PackageJsonFileName); - if (!File.Exists(packageJsonPath)) + var isGreenfield = !File.Exists(packageJsonPath); + if (isGreenfield) { // Greenfield: include root metadata so the scaffold output is a complete package.json. var packageName = request.ProjectName?.ToLowerInvariant() ?? "aspire-apphost"; @@ -165,6 +167,16 @@ private static string CreatePackageJson(ScaffoldRequest request) scripts["aspire:build"] = $"tsc -p {AppHostTsConfigFileName}"; scripts["aspire:dev"] = $"tsc --watch -p {AppHostTsConfigFileName}"; + if (isGreenfield) + { + scripts["lint"] = "npm run aspire:lint"; + scripts["predev"] = "npm run aspire:lint"; + scripts["dev"] = "npm run aspire:start"; + scripts["prebuild"] = "npm run aspire:lint"; + scripts["build"] = "npm run aspire:build"; + scripts["watch"] = "npm run aspire:dev"; + } + EnsureDependency(packageJson, "dependencies", "vscode-jsonrpc", "^8.2.0"); EnsureDependency(packageJson, "devDependencies", "@types/node", "^22.0.0"); EnsureDependency(packageJson, "devDependencies", "eslint", "^10.0.3"); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs index 38f133b34dc..c5f15576660 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs @@ -42,7 +42,10 @@ public async Task CreateAndRunPythonReactProject() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - // Step 3: Start and stop the project + // Step 3: Verify the generated TypeScript AppHost builds successfully. + await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2)); + + // Step 4: Start and stop the project await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs index 19cc6aa538a..847cd9126c0 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs @@ -40,6 +40,8 @@ public async Task CreateAndRunTypeScriptEmptyAppHostProject() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); + await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2)); + await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs index 34cab5277e3..864c1ed1db3 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs @@ -56,6 +56,8 @@ public async Task CreateAndRunTypeScriptStarterProject() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); + await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2)); + await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs index 1b016e144bc..fca7ec67176 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs @@ -39,9 +39,12 @@ public void Scaffold_CreatesAppHostSpecificScriptsAndTsConfig_ForNewProject() Assert.Equal("tsc -p tsconfig.apphost.json", scripts["aspire:build"]?.GetValue()); Assert.Equal("tsc --watch -p tsconfig.apphost.json", scripts["aspire:dev"]?.GetValue()); Assert.Equal("eslint apphost.ts", scripts["aspire:lint"]?.GetValue()); - Assert.False(scripts.ContainsKey("start")); - Assert.False(scripts.ContainsKey("build")); - Assert.False(scripts.ContainsKey("dev")); + Assert.Equal("npm run aspire:lint", scripts["lint"]?.GetValue()); + Assert.Equal("npm run aspire:lint", scripts["predev"]?.GetValue()); + Assert.Equal("npm run aspire:start", scripts["dev"]?.GetValue()); + Assert.Equal("npm run aspire:lint", scripts["prebuild"]?.GetValue()); + Assert.Equal("npm run aspire:build", scripts["build"]?.GetValue()); + Assert.Equal("npm run aspire:dev", scripts["watch"]?.GetValue()); Assert.Equal("^4.21.0", devDependencies["tsx"]?.GetValue()); Assert.Equal("^5.9.3", devDependencies["typescript"]?.GetValue()); Assert.Equal("^10.0.3", devDependencies["eslint"]?.GetValue()); @@ -54,6 +57,7 @@ public void Scaffold_CreatesAppHostSpecificScriptsAndTsConfig_ForNewProject() Assert.DoesNotContain("\\u003E", files["package.json"]); Assert.Contains("eslint.config.mjs", files.Keys); + Assert.Contains("project: './tsconfig.apphost.json'", files["eslint.config.mjs"]); var tsConfig = ParseJson(files["tsconfig.apphost.json"]); Assert.Equal("./dist/apphost", tsConfig["compilerOptions"]?["outDir"]?.GetValue()); From ee860bda2a0c813a9a07b26aa988ae7c279d5562 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 14:39:54 -0700 Subject: [PATCH 19/55] Fix sticky 'Finding apphosts' notification in VS Code extension (#16665) The 'Finding apphosts' message was displayed via DisplayMessage, which the extension renders as a sticky vscode.window.showInformationMessage toast that has no programmatic dismissal. Fold it into the surrounding ShowStatusAsync status text so it rides on the existing progress notification (auto-dismissed via the finally block) instead. Co-authored-by: Adam Ratzman --- src/Aspire.Cli/Projects/ProjectLocator.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Aspire.Cli/Projects/ProjectLocator.cs b/src/Aspire.Cli/Projects/ProjectLocator.cs index c63972698e8..eb740599312 100644 --- a/src/Aspire.Cli/Projects/ProjectLocator.cs +++ b/src/Aspire.Cli/Projects/ProjectLocator.cs @@ -51,7 +51,7 @@ public async Task> FindAppHostProjectFilesAsync(string searchDire { using var activity = telemetry.StartDiagnosticActivity(); - return await interactionService.ShowStatusAsync(InteractionServiceStrings.SearchingProjects, async () => + return await interactionService.ShowStatusAsync(InteractionServiceStrings.FindingAppHosts, async () => { var appHostProjects = new List(); var unbuildableSuspectedAppHostProjects = new List(); @@ -64,8 +64,6 @@ public async Task> FindAppHostProjectFilesAsync(string searchDire IgnoreInaccessible = true }; - interactionService.DisplayMessage(KnownEmojis.MagnifyingGlassTiltedLeft, InteractionServiceStrings.FindingAppHosts); - using var validationCancellationTokenSource = stopAfterMultipleBuildableAppHosts ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : null; From bd20f904cde09ecb9c9ae5116a6f13614bf2d7c2 Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 10:53:48 -0700 Subject: [PATCH 20/55] [release/13.3] API Review Feedback (#16674) * API Review Feedback Addressing feedback from https://github.com/microsoft/aspire/pull/16602 Rename JS experimental ID, refactor AKS/EF resource APIs Renames ASPIREEXTENSION001 to ASPIREJAVASCRIPT001 for JavaScript APIs, resources, and tests. Refactors AKS-related code to Aspire.Hosting.Azure.Kubernetes, makes AksSkuTier internal, and defaults AKS SKU tier to Free. Renames EFMigrationResource.ContextTypeName to DbContextTypeName throughout. Adds missing using directives, Experimental attributes, and AspireValue metadata. Updates tests and samples for new names and diagnostics. * Remove unused enum * Change more dbContextTypeName instances. * Update CodeGeneration snapshots for new WellKnownPipelineSteps entries Adding [AspireValue("WellKnownPipelineSteps")] to BeforeStart and CheckContainerRuntime causes the language code generators to emit them in the WellKnownPipelineSteps exported value catalog. Update the TwoPassScanningGeneratedAspire snapshots for Go, Java, Python, Rust, and TypeScript to include the two new entries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Eric Erhardt Co-authored-by: Jose Perez Rodriguez Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AspireJavaScript.AppHost/AppHost.cs | 8 +--- .../AspireWithNode.AppHost/AppHost.cs | 1 - .../AksSkuTier.cs | 25 ----------- .../AzureKubernetesEnvironmentExtensions.cs | 10 +---- ...bernetesEnvironmentResource.AksPipeline.cs | 3 +- .../AzureKubernetesEnvironmentResource.cs | 8 +--- .../AzureKubernetesIngressExtensions.cs | 2 +- .../EFMigrationResource.cs | 10 ++--- .../EFMigrationResourceBuilderExtensions.cs | 1 + .../EFResourceBuilderExtensions.cs | 43 ++++++++++--------- .../JavaScriptHostingExtensions.cs | 10 ++++- .../NextJsAppResource.cs | 3 ++ .../PublishAsStaticWebsiteOptions.cs | 3 ++ src/Aspire.Hosting/Dcp/ContainerCreator.cs | 1 - src/Aspire.Hosting/Dcp/DcpExecutor.cs | 1 - .../Pipelines/WellKnownPipelineSteps.cs | 2 + ...ureKubernetesEnvironmentExtensionsTests.cs | 1 - ...TwoPassScanningGeneratedAspire.verified.go | 4 ++ ...oPassScanningGeneratedAspire.verified.java | 6 +++ ...TwoPassScanningGeneratedAspire.verified.py | 4 ++ ...TwoPassScanningGeneratedAspire.verified.rs | 10 +++++ ...TwoPassScanningGeneratedAspire.verified.ts | 6 +++ .../AddEFMigrationsTests.cs | 16 +++---- .../EFMigrationCommandsTests.cs | 2 +- .../EFMigrationConfigurationTests.cs | 2 +- .../AddJavaScriptAppTests.cs | 2 +- .../AddNodeAppTests.cs | 2 + .../AddViteAppTests.cs | 2 +- .../NodeJsPublicApiTests.cs | 2 +- 29 files changed, 95 insertions(+), 95 deletions(-) delete mode 100644 src/Aspire.Hosting.Azure.Kubernetes/AksSkuTier.cs diff --git a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs index 1f4668d1147..3e2a1624b32 100644 --- a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs +++ b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs @@ -1,5 +1,3 @@ -#pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only - var builder = DistributedApplication.CreateBuilder(args); var weatherApi = builder.AddProject("weatherapi") @@ -32,22 +30,20 @@ .WithExternalHttpEndpoints() .PublishAsDockerFile(); -#pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var reactvite = builder.AddViteApp("reactvite", "../AspireJavaScript.Vite") .WithReference(weatherApi) .WithEnvironment("BROWSER", "none") .WithExternalHttpEndpoints(); -#pragma warning restore ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. // Demonstrate the new publish methods: // PublishAsStaticWebsite: deploys the Vite app as a static site served by YARP. // With apiPath/apiTarget, YARP reverse-proxies /api/* to the weather API via service discovery — no CORS needed. -#pragma warning disable ASPIREEXTENSION001 +#pragma warning disable ASPIREJAVASCRIPT001 builder.AddViteApp("vite-static", "../AspireJavaScript.Vite") .WithExternalHttpEndpoints() .PublishAsStaticWebsite("/api", weatherApi); -#pragma warning restore ASPIREEXTENSION001 +#pragma warning restore ASPIREJAVASCRIPT001 // PublishAsNodeServer: for frameworks that produce a self-contained Node.js server artifact. // Example: SvelteKit with adapter-node builds to build/index.js, Nuxt/TanStack build to .output/server/index.mjs diff --git a/playground/AspireWithNode/AspireWithNode.AppHost/AppHost.cs b/playground/AspireWithNode/AspireWithNode.AppHost/AppHost.cs index a779602c52f..c27d38d22a0 100644 --- a/playground/AspireWithNode/AspireWithNode.AppHost/AppHost.cs +++ b/playground/AspireWithNode/AspireWithNode.AppHost/AppHost.cs @@ -1,4 +1,3 @@ -#pragma warning disable ASPIREEXTENSION001 #pragma warning disable ASPIRECERTIFICATES001 using Microsoft.Extensions.Hosting; diff --git a/src/Aspire.Hosting.Azure.Kubernetes/AksSkuTier.cs b/src/Aspire.Hosting.Azure.Kubernetes/AksSkuTier.cs deleted file mode 100644 index f2f10865ac4..00000000000 --- a/src/Aspire.Hosting.Azure.Kubernetes/AksSkuTier.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Aspire.Hosting.Azure.Kubernetes; - -/// -/// Specifies the SKU tier for an AKS cluster. -/// -public enum AksSkuTier -{ - /// - /// Free tier with no SLA. - /// - Free, - - /// - /// Standard tier with financially backed SLA. - /// - Standard, - - /// - /// Premium tier with mission-critical features. - /// - Premium -} diff --git a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentExtensions.cs b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentExtensions.cs index 178c47b99a7..e722d41626a 100644 --- a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentExtensions.cs +++ b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentExtensions.cs @@ -377,14 +377,6 @@ private static void ConfigureAksInfrastructure(AzureResourceInfrastructure infra { var aksResource = (AzureKubernetesEnvironmentResource)infrastructure.AspireResource; - var skuTier = aksResource.SkuTier switch - { - AksSkuTier.Free => ManagedClusterSkuTier.Free, - AksSkuTier.Standard => ManagedClusterSkuTier.Standard, - AksSkuTier.Premium => ManagedClusterSkuTier.Premium, - _ => ManagedClusterSkuTier.Free - }; - // Create the AKS managed cluster var aks = new ContainerServiceManagedCluster(aksResource.GetBicepIdentifier()) { @@ -395,7 +387,7 @@ private static void ConfigureAksInfrastructure(AzureResourceInfrastructure infra Sku = new ManagedClusterSku { Name = ManagedClusterSkuName.Base, - Tier = skuTier + Tier = ManagedClusterSkuTier.Free }, DnsPrefix = $"{aksResource.Name}-dns", Tags = { { "aspire-resource-name", aksResource.Name } } diff --git a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs index 0ada6ba3fd2..335d47b6d4a 100644 --- a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs +++ b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs @@ -8,7 +8,6 @@ using System.Text; using System.Text.RegularExpressions; using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Azure.Kubernetes; using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Kubernetes; using Aspire.Hosting.Kubernetes.Resources; @@ -17,7 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Aspire.Hosting.Azure; +namespace Aspire.Hosting.Azure.Kubernetes; /// /// AKS-specific pipeline step implementations for . diff --git a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs index 2cce7bdb839..d43b6cdee3e 100644 --- a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs @@ -5,11 +5,10 @@ #pragma warning disable ASPIREPIPELINES001 #pragma warning disable ASPIREAZURE001 -using Aspire.Hosting.Azure.Kubernetes; using Aspire.Hosting.Kubernetes; using Aspire.Hosting.Pipelines; -namespace Aspire.Hosting.Azure; +namespace Aspire.Hosting.Azure.Kubernetes; /// /// Represents an Azure Kubernetes Service (AKS) environment resource that provisions @@ -112,11 +111,6 @@ public AzureKubernetesEnvironmentResource( /// internal string? KubernetesVersion { get; set; } - /// - /// Gets or sets the SKU tier for the AKS cluster. - /// - internal AksSkuTier SkuTier { get; set; } = AksSkuTier.Free; - /// /// Gets or sets whether OIDC issuer is enabled on the cluster. /// diff --git a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesIngressExtensions.cs b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesIngressExtensions.cs index abdbcc29c7e..2775956cadd 100644 --- a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesIngressExtensions.cs +++ b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesIngressExtensions.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Azure; +using Aspire.Hosting.Azure.Kubernetes; using Aspire.Hosting.Kubernetes; namespace Aspire.Hosting; diff --git a/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResource.cs b/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResource.cs index c0362204200..4aafb8c3c12 100644 --- a/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResource.cs +++ b/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResource.cs @@ -5,22 +5,22 @@ using Aspire.Hosting.ApplicationModel; -namespace Aspire.Hosting; +namespace Aspire.Hosting.EntityFrameworkCore; /// /// Represents an EF Core migration resource associated with a project. /// /// The name of the resource. /// The parent project resource that contains the DbContext. -/// The fully qualified name of the DbContext type, or null to auto-detect. +/// The fully qualified name of the DbContext type, or null to auto-detect. /// /// The resource inherits from so it can be published as a container image /// that runs the migration bundle at deploy time when -/// +/// /// is called with publishContainer: true. /// [AspireExport(ExposeProperties = true)] -public class EFMigrationResource(string name, ProjectResource projectResource, string? contextTypeName) +public class EFMigrationResource(string name, ProjectResource projectResource, string? dbContextTypeName) : ContainerResource(name) { /// @@ -35,7 +35,7 @@ public class EFMigrationResource(string name, ProjectResource projectResource, s /// This property is used to specify which DbContext to use when the project contains multiple DbContext types. /// When null, the EF Core tools will auto-detect the DbContext to use. /// - public string? ContextTypeName { get; } = contextTypeName; + public string? DbContextTypeName { get; } = dbContextTypeName; /// /// Gets or sets whether a migration script should be generated during publishing. diff --git a/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResourceBuilderExtensions.cs b/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResourceBuilderExtensions.cs index c219697862b..c8c6d396766 100644 --- a/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.EntityFrameworkCore/EFMigrationResourceBuilderExtensions.cs @@ -4,6 +4,7 @@ #pragma warning disable ASPIREPIPELINES001 // PipelineStepAnnotation is experimental; used to wire migration-bundle pipeline steps. using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.EntityFrameworkCore; using Aspire.Hosting.Pipelines; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/Aspire.Hosting.EntityFrameworkCore/EFResourceBuilderExtensions.cs b/src/Aspire.Hosting.EntityFrameworkCore/EFResourceBuilderExtensions.cs index 30ba737a0f2..e8e02908d10 100644 --- a/src/Aspire.Hosting.EntityFrameworkCore/EFResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.EntityFrameworkCore/EFResourceBuilderExtensions.cs @@ -5,6 +5,7 @@ #pragma warning disable ASPIREDOTNETTOOL using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.EntityFrameworkCore; using Aspire.Hosting.Pipelines; using System.Diagnostics; using System.Text; @@ -33,7 +34,7 @@ private static string GetShortTypeName(string? fullTypeName) /// /// The resource builder for the project. /// The name of the migration resource. - /// The fully qualified name of the DbContext type to manage migrations for. + /// The fully qualified name of the DbContext type to manage migrations for. /// An EF migration resource builder for chaining additional configuration. /// Thrown if migrations for this context type have already been added. /// @@ -50,13 +51,13 @@ private static string GetShortTypeName(string? fullTypeName) public static IResourceBuilder AddEFMigrations( this IResourceBuilder builder, [ResourceName] string name, - string contextTypeName) + string dbContextTypeName) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); - ArgumentException.ThrowIfNullOrEmpty(contextTypeName); + ArgumentException.ThrowIfNullOrEmpty(dbContextTypeName); - return AddEFMigrationsCore(builder, name, contextTypeName, configureToolResource: null); + return AddEFMigrationsCore(builder, name, dbContextTypeName, configureToolResource: null); } /// @@ -64,7 +65,7 @@ public static IResourceBuilder AddEFMigrations( /// /// The resource builder for the project. /// The name of the migration resource. - /// The fully qualified name of the DbContext type to manage migrations for. + /// The fully qualified name of the DbContext type to manage migrations for. /// Optional callback to configure the dotnet-ef tool resource used for migrations. /// An EF migration resource builder for chaining additional configuration. /// Thrown if migrations for this context type have already been added. @@ -82,14 +83,14 @@ public static IResourceBuilder AddEFMigrations( public static IResourceBuilder AddEFMigrations( this IResourceBuilder builder, [ResourceName] string name, - string contextTypeName, + string dbContextTypeName, Action>? configureToolResource) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); - ArgumentException.ThrowIfNullOrEmpty(contextTypeName); + ArgumentException.ThrowIfNullOrEmpty(dbContextTypeName); - return AddEFMigrationsCore(builder, name, contextTypeName, configureToolResource); + return AddEFMigrationsCore(builder, name, dbContextTypeName, configureToolResource); } /// @@ -106,7 +107,7 @@ public static IResourceBuilder AddEFMigrations( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); - return AddEFMigrationsCore(builder, name, contextTypeName: null, configureToolResource: null); + return AddEFMigrationsCore(builder, name, dbContextTypeName: null, configureToolResource: null); } /// @@ -125,13 +126,13 @@ public static IResourceBuilder AddEFMigrations( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); - return AddEFMigrationsCore(builder, name, contextTypeName: null, configureToolResource); + return AddEFMigrationsCore(builder, name, dbContextTypeName: null, configureToolResource); } private static IResourceBuilder AddEFMigrationsCore( IResourceBuilder builder, string name, - string? contextTypeName, + string? dbContextTypeName, Action>? configureToolResource) { // Check for duplicate context types and null/non-null conflicts @@ -140,15 +141,15 @@ private static IResourceBuilder AddEFMigrationsCore( .Where(r => r.ProjectResource == builder.Resource) .ToList(); - if (contextTypeName != null) + if (dbContextTypeName != null) { - if (existingMigrations.Any(r => r.ContextTypeName == contextTypeName)) + if (existingMigrations.Any(r => r.DbContextTypeName == dbContextTypeName)) { throw new InvalidOperationException( - $"The DbContext type '{GetShortTypeName(contextTypeName)}' has already been registered for EF migrations on resource '{builder.Resource.Name}'."); + $"The DbContext type '{GetShortTypeName(dbContextTypeName)}' has already been registered for EF migrations on resource '{builder.Resource.Name}'."); } - if (existingMigrations.Any(r => r.ContextTypeName == null)) + if (existingMigrations.Any(r => r.DbContextTypeName == null)) { throw new InvalidOperationException( $"Cannot add migrations for a specific DbContext type when auto-detected migrations have already been registered on resource '{builder.Resource.Name}'."); @@ -163,7 +164,7 @@ private static IResourceBuilder AddEFMigrationsCore( } } - var migrationResource = new EFMigrationResource(name, builder.Resource, contextTypeName) + var migrationResource = new EFMigrationResource(name, builder.Resource, dbContextTypeName) { ConfigureToolResource = configureToolResource }; @@ -180,7 +181,7 @@ private static IResourceBuilder AddEFMigrationsCore( .WithIconName("Database") .WithPipelineStepFactory(CreateMigrationPipelineStep); - AddEFMigrationCommands(innerBuilder, migrationResource, contextTypeName); + AddEFMigrationCommands(innerBuilder, migrationResource, dbContextTypeName); return innerBuilder; } @@ -270,7 +271,7 @@ private static async Task ExecutePublishPipelineOperationAsync( using var executor = new EFCoreOperationExecutor( migrationResource.ProjectResource, migrationResource.MigrationsProjectPath, - migrationResource.ContextTypeName, + migrationResource.DbContextTypeName, logger, stepContext.CancellationToken, stepContext.Services, @@ -520,9 +521,9 @@ await notificationService.PublishUpdateAsync(toolResource, s => s with private static void AddEFMigrationCommands( IResourceBuilder migrationBuilder, EFMigrationResource migrationResource, - string? contextTypeName) + string? dbContextTypeName) { - var contextShortName = GetShortTypeName(contextTypeName); + var contextShortName = GetShortTypeName(dbContextTypeName); // Create hidden DotnetToolResource for running EF commands var toolName = $"ef-tool-{migrationResource.Name}"; @@ -718,7 +719,7 @@ private static async Task ExecuteWithStateManagementAsync( using var executor = new EFCoreOperationExecutor( migrationResource.ProjectResource, migrationResource.MigrationsProjectPath, - migrationResource.ContextTypeName, + migrationResource.DbContextTypeName, logger, context.CancellationToken, context.ServiceProvider, diff --git a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs index 8bcdfbde18a..2f36f3f8bd2 100644 --- a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs +++ b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs @@ -355,7 +355,7 @@ public static IResourceBuilder AddJavaScriptApp(this IDis /// To add an API reverse-proxy, use the overload that accepts an apiPath and apiTarget. /// /// - [Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExportIgnore(Reason = "Use the polyglot-compatible overload instead.")] public static IResourceBuilder PublishAsStaticWebsite( this IResourceBuilder builder, @@ -390,7 +390,7 @@ public static IResourceBuilder PublishAsStaticWebsite( /// work correctly across all deployment targets (Docker Compose, Azure App Service, etc.). /// /// - [Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExportIgnore(Reason = "Use the polyglot-compatible overload instead.")] public static IResourceBuilder PublishAsStaticWebsite( this IResourceBuilder builder, @@ -410,6 +410,7 @@ public static IResourceBuilder PublishAsStaticWebsite( /// in a single options object rather than positional args. /// #pragma warning disable ASPIREEXPORT009 // Polyglot entry point — collision is intentional + [Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport("publishAsStaticWebsite", Description = "Publishes the JavaScript application as a standalone static website using YARP.")] internal static IResourceBuilder PublishAsStaticWebsitePolyglot( #pragma warning restore ASPIREEXPORT009 @@ -430,6 +431,7 @@ internal static IResourceBuilder PublishAsStaticWebsitePolyglot PublishAsStaticWebsiteCore( IResourceBuilder builder, string? apiPath, @@ -542,6 +544,7 @@ private static IResourceBuilder PublishAsStaticWebsiteCore /// the built output directory is copied into the runtime container, not the full application source. /// /// + [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.")] public static IResourceBuilder PublishAsNodeServer(this IResourceBuilder builder, string entryPoint, string outputPath = ".") where TResource : JavaScriptAppResource @@ -605,6 +608,7 @@ public static IResourceBuilder PublishAsNodeServer(this IR /// use instead for a smaller runtime image. /// /// + [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 IResourceBuilder PublishAsNpmScript(this IResourceBuilder builder, string startScriptName = "start", string? runScriptArguments = null) where TResource : JavaScriptAppResource @@ -1084,6 +1088,7 @@ public static IResourceBuilder AddViteApp(this IDistributedAppl /// /// /// + [Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport(Description = "Adds a Next.js application resource")] public static IResourceBuilder AddNextJsApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string appDirectory, string runScriptName = "dev") { @@ -1170,6 +1175,7 @@ public static IResourceBuilder AddNextJsApp(this IDistributed /// to suppress those checks when the configuration is set dynamically or via an external /// mechanism that cannot be detected by static file inspection. /// + [Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport(Description = "Disables deploy-time build validation checks for the Next.js application.")] public static IResourceBuilder DisableBuildValidation(this IResourceBuilder builder) { diff --git a/src/Aspire.Hosting.JavaScript/NextJsAppResource.cs b/src/Aspire.Hosting.JavaScript/NextJsAppResource.cs index 60a2b3dc681..3c776a8a018 100644 --- a/src/Aspire.Hosting.JavaScript/NextJsAppResource.cs +++ b/src/Aspire.Hosting.JavaScript/NextJsAppResource.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.Diagnostics.CodeAnalysis; + namespace Aspire.Hosting.JavaScript; /// @@ -10,5 +12,6 @@ namespace Aspire.Hosting.JavaScript; /// The command to execute the application. /// The working directory from which the application command is executed. [AspireExport(ExposeProperties = true)] +[Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public class NextJsAppResource(string name, string command, string workingDirectory) : JavaScriptAppResource(name, command, workingDirectory); diff --git a/src/Aspire.Hosting.JavaScript/PublishAsStaticWebsiteOptions.cs b/src/Aspire.Hosting.JavaScript/PublishAsStaticWebsiteOptions.cs index cf7bc9ded06..ee8c40fbbd0 100644 --- a/src/Aspire.Hosting.JavaScript/PublishAsStaticWebsiteOptions.cs +++ b/src/Aspire.Hosting.JavaScript/PublishAsStaticWebsiteOptions.cs @@ -1,11 +1,14 @@ // 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; + namespace Aspire.Hosting.JavaScript; /// /// Options for configuring the static website publish mode. /// +[Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public class PublishAsStaticWebsiteOptions { /// diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index fe377d8bef6..a9364dcf69e 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -1,7 +1,6 @@ // 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 ASPIREEXTENSION001 #pragma warning disable ASPIRECERTIFICATES001 #pragma warning disable ASPIRECONTAINERSHELLEXECUTION001 diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index c9e730c4fcc..49c76b4b9fa 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -1,7 +1,6 @@ // 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 ASPIREEXTENSION001 #pragma warning disable ASPIRECERTIFICATES001 #pragma warning disable ASPIRECONTAINERSHELLEXECUTION001 diff --git a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs index dca3b069fcb..4b3f0b97459 100644 --- a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs +++ b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs @@ -83,12 +83,14 @@ public static class WellKnownPipelineSteps /// /// The step that runs before the application starts. /// + [AspireValue("WellKnownPipelineSteps")] public const string BeforeStart = "before-start"; /// /// The step that checks whether the container runtime (e.g., Docker or Podman) is running. /// Build steps that need a container runtime should depend on this step. /// + [AspireValue("WellKnownPipelineSteps")] public const string CheckContainerRuntime = "check-container-runtime"; /// diff --git a/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs index 0332d4c4c6a..4fdfe112cea 100644 --- a/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs @@ -76,7 +76,6 @@ public void AddAzureKubernetesEnvironment_DefaultConfiguration() Assert.True(aks.Resource.OidcIssuerEnabled); Assert.True(aks.Resource.WorkloadIdentityEnabled); - Assert.Equal(AksSkuTier.Free, aks.Resource.SkuTier); Assert.Null(aks.Resource.KubernetesVersion); Assert.False(aks.Resource.IsPrivateCluster); Assert.False(aks.Resource.ContainerInsightsEnabled); diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index a65935ed11d..cd14e7465d5 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -554,8 +554,10 @@ var TestConfigs = struct { } var WellKnownPipelineSteps = struct { + BeforeStart string Build string BuildPrereq string + CheckContainerRuntime string Deploy string DeployPrereq string Destroy string @@ -568,8 +570,10 @@ var WellKnownPipelineSteps = struct { PushPrereq string ValidateComputeEnvironments string }{ + BeforeStart: "before-start", Build: "build", BuildPrereq: "build-prereq", + CheckContainerRuntime: "check-container-runtime", Deploy: "deploy", DeployPrereq: "deploy-prereq", Destroy: "destroy", diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 7abe7db08b1..be9e60bed26 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -21796,12 +21796,18 @@ public static WaitBehavior fromValue(String value) { public final class WellKnownPipelineSteps { private WellKnownPipelineSteps() { } + /** The step that runs before the application starts. */ + public static final String BeforeStart = "before-start"; + /** The well-known step for building resources. */ public static final String Build = "build"; /** The prerequisite step that runs before any build operations. */ public static final String BuildPrereq = "build-prereq"; + /** The step that checks whether the container runtime (e.g., Docker or Podman) is running. Build steps that need a container runtime should depend on this step. */ + public static final String CheckContainerRuntime = "check-container-runtime"; + /** Aggregation step for all deploy operations. All deploy steps should be required by this step. */ public static final String Deploy = "deploy"; diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 94ade19a23c..1a8900f2962 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1832,10 +1832,14 @@ class TestNestedDto(typing.TypedDict, total=False): TestConfigs.UnicodeGreeting = "你好こんにちは" WellKnownPipelineSteps = types.SimpleNamespace() +# The step that runs before the application starts. +WellKnownPipelineSteps.BeforeStart = "before-start" # The well-known step for building resources. WellKnownPipelineSteps.Build = "build" # The prerequisite step that runs before any build operations. WellKnownPipelineSteps.BuildPrereq = "build-prereq" +# The step that checks whether the container runtime (e.g., Docker or Podman) is running. Build steps that need a container runtime should depend on this step. +WellKnownPipelineSteps.CheckContainerRuntime = "check-container-runtime" # Aggregation step for all deploy operations. All deploy steps should be required by this step. WellKnownPipelineSteps.Deploy = "deploy" # The prerequisite step that runs before any deploy operations. diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index bcda4b00ae6..17afdaf74e6 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -941,6 +941,11 @@ pub mod test_configs { pub mod well_known_pipeline_steps { use super::*; + /// The step that runs before the application starts. + pub fn before_start() -> String { + serde_json::from_value::(serde_json::json!("before-start")) + .expect("generated exported value should deserialize") + } /// The well-known step for building resources. pub fn build() -> String { serde_json::from_value::(serde_json::json!("build")) @@ -951,6 +956,11 @@ pub mod well_known_pipeline_steps { serde_json::from_value::(serde_json::json!("build-prereq")) .expect("generated exported value should deserialize") } + /// The step that checks whether the container runtime (e.g., Docker or Podman) is running. Build steps that need a container runtime should depend on this step. + pub fn check_container_runtime() -> String { + serde_json::from_value::(serde_json::json!("check-container-runtime")) + .expect("generated exported value should deserialize") + } /// Aggregation step for all deploy operations. All deploy steps should be required by this step. pub fn deploy() -> String { serde_json::from_value::(serde_json::json!("deploy")) diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index fade7da509f..68823e3ab33 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -605,12 +605,18 @@ export namespace TestConfigs { } export namespace WellKnownPipelineSteps { + /** The step that runs before the application starts. */ + export const BeforeStart = "before-start"; + /** The well-known step for building resources. */ export const Build = "build"; /** The prerequisite step that runs before any build operations. */ export const BuildPrereq = "build-prereq"; + /** The step that checks whether the container runtime (e.g., Docker or Podman) is running. Build steps that need a container runtime should depend on this step. */ + export const CheckContainerRuntime = "check-container-runtime"; + /** Aggregation step for all deploy operations. All deploy steps should be required by this step. */ export const Deploy = "deploy"; diff --git a/tests/Aspire.Hosting.EntityFrameworkCore.Tests/AddEFMigrationsTests.cs b/tests/Aspire.Hosting.EntityFrameworkCore.Tests/AddEFMigrationsTests.cs index 7831942ebbc..550056c0935 100644 --- a/tests/Aspire.Hosting.EntityFrameworkCore.Tests/AddEFMigrationsTests.cs +++ b/tests/Aspire.Hosting.EntityFrameworkCore.Tests/AddEFMigrationsTests.cs @@ -20,7 +20,7 @@ public void AddEFMigrationsCreatesResource() Assert.IsAssignableFrom>(migrations); Assert.Equal("mymigrations", migrations.Resource.Name); Assert.Equal(project.Resource, migrations.Resource.ProjectResource); - Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.ContextTypeName); + Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.DbContextTypeName); } [Fact] @@ -34,7 +34,7 @@ public void AddEFMigrationsWithoutContextTypeCreatesResource() Assert.IsAssignableFrom>(migrations); Assert.Equal("mymigrations", migrations.Resource.Name); Assert.Equal(project.Resource, migrations.Resource.ProjectResource); - Assert.Null(migrations.Resource.ContextTypeName); + Assert.Null(migrations.Resource.DbContextTypeName); } [Fact] @@ -48,7 +48,7 @@ public void AddEFMigrationsWithExplicitContextTypeNameCreatesResource() Assert.IsAssignableFrom>(migrations); Assert.Equal("mymigrations", migrations.Resource.Name); Assert.Equal(project.Resource, migrations.Resource.ProjectResource); - Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.ContextTypeName); + Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.DbContextTypeName); } [Fact] @@ -63,7 +63,7 @@ public void AddEFMigrationsWithContextTypeNameStringCreatesResource() Assert.IsAssignableFrom>(migrations); Assert.Equal("mymigrations", migrations.Resource.Name); Assert.Equal(project.Resource, migrations.Resource.ProjectResource); - Assert.Equal(contextTypeName, migrations.Resource.ContextTypeName); + Assert.Equal(contextTypeName, migrations.Resource.DbContextTypeName); } [Fact] @@ -91,8 +91,8 @@ public void AddEFMigrationsForMultipleContextsSucceeds() var migrations2 = project.AddEFMigrations("migrations2", typeof(AnotherDbContext).FullName!); Assert.NotEqual(migrations1.Resource, migrations2.Resource); - Assert.Equal(typeof(TestDbContext).FullName, migrations1.Resource.ContextTypeName); - Assert.Equal(typeof(AnotherDbContext).FullName, migrations2.Resource.ContextTypeName); + Assert.Equal(typeof(TestDbContext).FullName, migrations1.Resource.DbContextTypeName); + Assert.Equal(typeof(AnotherDbContext).FullName, migrations2.Resource.DbContextTypeName); } [Fact] @@ -105,8 +105,8 @@ public void AddEFMigrationsForMultipleContextsWithStringNamesSucceeds() var migrations2 = project.AddEFMigrations("migrations2", "MyApp.Data.LoggingDbContext"); Assert.NotEqual(migrations1.Resource, migrations2.Resource); - Assert.Equal("MyApp.Data.AppDbContext", migrations1.Resource.ContextTypeName); - Assert.Equal("MyApp.Data.LoggingDbContext", migrations2.Resource.ContextTypeName); + Assert.Equal("MyApp.Data.AppDbContext", migrations1.Resource.DbContextTypeName); + Assert.Equal("MyApp.Data.LoggingDbContext", migrations2.Resource.DbContextTypeName); } [Fact] diff --git a/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationCommandsTests.cs b/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationCommandsTests.cs index f2a4eb33e3a..89ce6ec5ba6 100644 --- a/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationCommandsTests.cs +++ b/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationCommandsTests.cs @@ -132,7 +132,7 @@ public void AddEFMigrationsWithContextUsesContextTypeName() var migrations = project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!); // Verify the context type name is stored on the resource - Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.ContextTypeName); + Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.DbContextTypeName); } [Fact] diff --git a/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationConfigurationTests.cs b/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationConfigurationTests.cs index 486d6a9886d..d59a06233ab 100644 --- a/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationConfigurationTests.cs +++ b/tests/Aspire.Hosting.EntityFrameworkCore.Tests/EFMigrationConfigurationTests.cs @@ -126,7 +126,7 @@ public void ConfigurationMethodsPreserveContextTypeName() .PublishAsMigrationBundle(); // The context type name should be preserved through chaining - Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.ContextTypeName); + Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.DbContextTypeName); } [Fact] diff --git a/tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs b/tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs index 5edfb78a38a..2302c6a935f 100644 --- a/tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs +++ b/tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs @@ -1,7 +1,7 @@ // 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 ASPIREEXTENSION001 // Type is for evaluation purposes only +#pragma warning disable ASPIREJAVASCRIPT001 // Type is for evaluation purposes only using System.Diagnostics; using Aspire.Hosting.ApplicationModel; diff --git a/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs b/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs index 8bfe3f07720..7d5c7449d1a 100644 --- a/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs +++ b/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs @@ -429,6 +429,7 @@ private sealed class MyFilesContainer(string name, string command, string workin : ExecutableResource(name, command, workingDirectory), IResourceWithContainerFiles; #pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only +#pragma warning disable ASPIREJAVASCRIPT001 // Type is for evaluation purposes only [Fact] public void NodeApp_WithVSCodeDebugging_AddsSupportsDebuggingAnnotation() @@ -589,4 +590,5 @@ private static IResourceBuilder InvokeWithReference( } #pragma warning restore ASPIREEXTENSION001 // Type is for evaluation purposes only +#pragma warning restore ASPIREJAVASCRIPT001 // Type is for evaluation purposes only } diff --git a/tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs b/tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs index d3a35b219b2..f7245991afb 100644 --- a/tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs +++ b/tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs @@ -4,7 +4,7 @@ #pragma warning disable ASPIREDOCKERFILEBUILDER001 // Type is for evaluation purposes only #pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only #pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only -#pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only +#pragma warning disable ASPIREJAVASCRIPT001 // Type is for evaluation purposes only using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Pipelines; diff --git a/tests/Aspire.Hosting.JavaScript.Tests/NodeJsPublicApiTests.cs b/tests/Aspire.Hosting.JavaScript.Tests/NodeJsPublicApiTests.cs index b990aaabefe..98d5ca199c1 100644 --- a/tests/Aspire.Hosting.JavaScript.Tests/NodeJsPublicApiTests.cs +++ b/tests/Aspire.Hosting.JavaScript.Tests/NodeJsPublicApiTests.cs @@ -1,7 +1,7 @@ // 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 ASPIREEXTENSION001 // Type is for evaluation purposes only +#pragma warning disable ASPIREJAVASCRIPT001 // Type is for evaluation purposes only using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; From 5bd693ae1897dee5e2ce71c2cc08879c1c7eff51 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 5 May 2026 02:14:25 +0800 Subject: [PATCH 21/55] [release/13.3] Normalize *.localhost dashboard URLs to localhost for CLI HTTP requests (#16708) * Normalize *.localhost dashboard URLs to localhost for HTTP requests DNS resolvers typically don't implement RFC 6761 for localhost subdomains, so hosts like 'myapp.dev.localhost' fail to resolve. This adds NormalizeDashboardUrl to McpToolHelpers which rewrites *.localhost API base URLs to localhost before making HTTP requests, while preserving the original hostname in dashboard display URLs (used in JSON output hyperlinks). - Add NormalizeDashboardUrl and IsLocalhostTld to McpToolHelpers - Normalize API base URL in TelemetryCommandHelpers and McpToolHelpers - Preserve original *.dev.localhost hostname in dashboard display URLs - Add unit tests for logs/traces with dev.localhost backchannel URLs - Add E2E test variant using dev.localhost dashboard URL * Refactor E2E test to use aspire ps --format json for dashboard URL * Fix E2E test: use --frontend-url and extract dashboard URL from logs - Use --frontend-url instead of invalid --dashboard-url for aspire dashboard run - Extract dashboard login URL from log output instead of aspire ps (which cannot discover standalone dashboards) - Pass login?t=xxx URL to aspire otel traces for proper token exchange - dev.localhost variant passes *.localhost to --frontend-url to exercise NormalizeDashboardUrl end-to-end * Fix E2E test: extract URL from dashboard run output, use --allow-anonymous - Add --allow-anonymous to avoid token exchange issues in standalone mode - Extract dashboard URL from 'Now listening on:' in aspire dashboard run output - Avoids needing login token exchange which fails in container E2E tests * Fix E2E test: use auth with login token from dashboard run output - Remove --allow-anonymous to verify authentication works end-to-end - Extract login URL (with ?t=xxx token) from aspire dashboard run output - CLI exchanges login token for API key via validateToken endpoint * Add echo of OTEL_DASHBOARD_URL for debugging visibility * Add AppHost otel traces tests with dev.localhost variant - Add AppHostOtelTracesReturnsTraces and _DevLocalhost variants - New tests create Starter project, start AppHost, generate traces, verify otel traces output - DevLocalhost variant uses useDevLocalhost flag in AspireNewAsync to test URL normalization - Add useDevLocalhost parameter to AspireNewAsync helper * Fix standalone dashboard tests and add dev.localhost otel logs test - Fix grep regex to use [a-f0-9]+ for token to avoid Spectre Console link duplication - Remove AppHost otel traces tests (covered by OtelLogs tests) - Add OtelLogsReturnsStructuredLogsFromStarterApp_DevLocalhost test * Use DASHBOARD__FRONTEND__BROWSERTOKEN instead of parsing URL from logs Spectre Console OSC 8 escape sequences in redirected output cause grep to capture a doubled URL. Instead, set a known browser token via env var and construct the dashboard URL directly. * Fix AspireStartAsync sed pattern to match *.localhost dashboard URLs The sed pattern for extracting dashboardUrl from JSON only matched 'localhost' literally. With dev.localhost subdomains (e.g. dashboard.dev.localhost:18888), the pattern failed to match, causing 'aspire start did not return a dashboard URL'. Broadened to [a-z.]*localhost. * Preserve original *.localhost hostname in display URLs for --dashboard-url path The --dashboard-url code path was normalizing the URL and using it for both HTTP requests and JSON output hyperlinks. Now it preserves the original hostname (e.g. dashboard.dev.localhost) for display URLs while still normalizing to localhost for HTTP requests, matching the backchannel path behavior. Also adds remarks to NormalizeDashboardUrl explaining the RFC 6761 DNS resolution motivation, and tests for the --dashboard-url display URL preservation. * Address review comments: normalize URL in StaticDashboardInfoProvider, fix error messages, fix E2E test - Normalize apiBaseUrl in StaticDashboardInfoProvider so aspire agent mcp --dashboard-url with *.dev.localhost URLs works correctly - Use displayDashboardUrl in error messages so users see the URL they typed - Fix E2E test to pass frontendUrl to aspire otel traces, actually exercising the NormalizeDashboardUrl code path end-to-end * Add test asserting error messages show original *.dev.localhost URL * Add E2E tests for aspire agent mcp list_structured_logs * Rename DashboardOtelTracesTests to DashboardRunTests and add agent mcp tests - Rename class and file to DashboardRunTests - Add E2E tests for aspire agent mcp --dashboard-url against standalone dashboard - Extract CallAgentMcpToolAsync helper to CliE2EAutomatorHelpers - Use shared helper in AgentMcpLogsTests --- .../Commands/TelemetryCommandHelpers.cs | 16 +- .../Mcp/Tools/IDashboardInfoProvider.cs | 5 +- src/Aspire.Cli/Mcp/Tools/McpToolHelpers.cs | 29 ++- .../AgentMcpLogsTests.cs | 78 ++++++ .../DashboardOtelTracesTests.cs | 87 ------- .../DashboardRunTests.cs | 177 +++++++++++++ .../Helpers/CliE2EAutomatorHelpers.cs | 55 +++- .../OtelLogsTests.cs | 17 +- .../Commands/TelemetryLogsCommandTests.cs | 234 ++++++++++++++++++ .../Commands/TelemetryTracesCommandTests.cs | 197 +++++++++++++++ tests/Shared/Hex1bAutomatorTestHelpers.cs | 12 +- 11 files changed, 803 insertions(+), 104 deletions(-) create mode 100644 tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs delete mode 100644 tests/Aspire.Cli.EndToEnd.Tests/DashboardOtelTracesTests.cs create mode 100644 tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs diff --git a/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs b/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs index 7ee0c597702..a55c053c8ec 100644 --- a/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs +++ b/src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs @@ -205,7 +205,8 @@ public static async Task GetDashboardApiAsync( var loginToken = McpToolHelpers.ExtractLoginToken(dashboardUrl); // Normalize login URLs (e.g., http://localhost:18888/login?t=abc) to base URL - dashboardUrl = McpToolHelpers.StripLoginPath(dashboardUrl) ?? dashboardUrl; + var displayDashboardUrl = McpToolHelpers.StripLoginPath(dashboardUrl) ?? dashboardUrl; + dashboardUrl = McpToolHelpers.NormalizeDashboardUrl(displayDashboardUrl); if (!UrlHelper.IsHttpUrl(dashboardUrl)) { @@ -229,10 +230,10 @@ public static async Task GetDashboardApiAsync( var errorInfo = exchangeResult.FailureKind switch { TokenExchangeFailureKind.ConnectionError => new TelemetryErrorInfo( - string.Format(CultureInfo.CurrentCulture, TelemetryCommandStrings.DashboardConnectionFailed, dashboardUrl), + string.Format(CultureInfo.CurrentCulture, TelemetryCommandStrings.DashboardConnectionFailed, displayDashboardUrl), TelemetryCommandStrings.DashboardConnectionFailedHint), TokenExchangeFailureKind.ApiNotEnabled => new TelemetryErrorInfo( - string.Format(CultureInfo.CurrentCulture, TelemetryCommandStrings.DashboardApiNotEnabled, dashboardUrl), + string.Format(CultureInfo.CurrentCulture, TelemetryCommandStrings.DashboardApiNotEnabled, displayDashboardUrl), TelemetryCommandStrings.DashboardApiNotEnabledHint), _ => new TelemetryErrorInfo( TelemetryCommandStrings.DashboardLoginTokenFailed, @@ -247,7 +248,7 @@ public static async Task GetDashboardApiAsync( } var token = apiKey ?? string.Empty; - return new DashboardApiResult(true, null, dashboardUrl, token, dashboardUrl, 0); + return new DashboardApiResult(true, null, dashboardUrl, token, displayDashboardUrl, 0); } var result = await connectionResolver.ResolveConnectionAsync( @@ -282,10 +283,13 @@ public static async Task GetDashboardApiAsync( return new DashboardApiResult(true, connection, null, null, null, 0); } - // Extract dashboard base URL (without /login path) for hyperlinks + var apiBaseUrl = McpToolHelpers.NormalizeDashboardUrl(dashboardInfo.ApiBaseUrl); + + // Extract dashboard base URL (without /login path) for hyperlinks. + // Preserve the original hostname (e.g. *.dev.localhost) for display URLs. var extractedDashboardUrl = ExtractDashboardBaseUrl(dashboardInfo.DashboardUrls?.FirstOrDefault()); - return new DashboardApiResult(true, connection, dashboardInfo.ApiBaseUrl, dashboardInfo.ApiToken, extractedDashboardUrl, 0); + return new DashboardApiResult(true, connection, apiBaseUrl, dashboardInfo.ApiToken, extractedDashboardUrl, 0); } /// diff --git a/src/Aspire.Cli/Mcp/Tools/IDashboardInfoProvider.cs b/src/Aspire.Cli/Mcp/Tools/IDashboardInfoProvider.cs index de2c6b215ed..271e83fe823 100644 --- a/src/Aspire.Cli/Mcp/Tools/IDashboardInfoProvider.cs +++ b/src/Aspire.Cli/Mcp/Tools/IDashboardInfoProvider.cs @@ -49,6 +49,9 @@ internal sealed class StaticDashboardInfoProvider(string dashboardUrl, string? a { // For unsecured dashboards, apiToken is empty string (no X-API-Key header will be sent) var apiToken = apiKey ?? string.Empty; - return Task.FromResult((apiToken, dashboardUrl, (string?)dashboardUrl)); + // Normalize the API base URL (e.g., rewrite *.localhost to localhost) for HTTP requests, + // but preserve the original URL as the dashboard display URL. + var apiBaseUrl = McpToolHelpers.NormalizeDashboardUrl(dashboardUrl); + return Task.FromResult((apiToken, apiBaseUrl, (string?)dashboardUrl)); } } diff --git a/src/Aspire.Cli/Mcp/Tools/McpToolHelpers.cs b/src/Aspire.Cli/Mcp/Tools/McpToolHelpers.cs index 5d3cd971edc..5eb6697ed41 100644 --- a/src/Aspire.Cli/Mcp/Tools/McpToolHelpers.cs +++ b/src/Aspire.Cli/Mcp/Tools/McpToolHelpers.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.Globalization; using System.Web; using Aspire.Cli.Backchannel; using Microsoft.Extensions.Logging; @@ -26,9 +27,10 @@ internal static class McpToolHelpers throw new McpProtocolException(McpErrorMessages.DashboardNotAvailable, McpErrorCode.InternalError); } + var apiBaseUrl = NormalizeDashboardUrl(dashboardInfo.ApiBaseUrl); var dashboardBaseUrl = StripLoginPath(dashboardInfo.DashboardUrls.FirstOrDefault()); - return (dashboardInfo.ApiToken, dashboardInfo.ApiBaseUrl, dashboardBaseUrl); + return (dashboardInfo.ApiToken, apiBaseUrl, dashboardBaseUrl); } /// @@ -59,6 +61,31 @@ internal static class McpToolHelpers return url; } + /// + /// Replaces AppHost-scoped *.localhost dashboard hostnames with localhost. + /// + /// + /// DNS resolvers typically don't implement RFC 6761 for localhost subdomains, so hosts + /// like dashboard.dev.localhost fail to resolve when making HTTP requests. + /// Rewriting to localhost ensures the CLI can reach the dashboard API. + /// + internal static string NormalizeDashboardUrl(string url) + { + if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && IsLocalhostTld(uri.Host)) + { + var port = uri.IsDefaultPort ? string.Empty : ":" + uri.Port.ToString(CultureInfo.InvariantCulture); + var pathAndQuery = uri.PathAndQuery == "/" ? string.Empty : uri.PathAndQuery; + return $"{uri.Scheme}://localhost{port}{pathAndQuery}{uri.Fragment}"; + } + + return url; + } + + private static bool IsLocalhostTld(string host) + { + return host.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase); + } + /// /// Extracts the browser token (t query parameter) from a dashboard login URL. /// Returns null if the URL does not contain a login token. diff --git a/tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs new file mode 100644 index 00000000000..53e2d3677b6 --- /dev/null +++ b/tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs @@ -0,0 +1,78 @@ +// 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.EndToEnd.Tests.Helpers; +using Aspire.Cli.Tests.Utils; +using Hex1b.Automation; +using Xunit; + +namespace Aspire.Cli.EndToEnd.Tests; + +/// +/// End-to-end tests for the aspire agent mcp command with structured logs. +/// Each test class runs as a separate CI job for parallelization. +/// +public sealed class AgentMcpLogsTests(ITestOutputHelper output) +{ + [Fact] + [CaptureWorkspaceOnFailure] + public Task AgentMcpListStructuredLogsReturnsLogsFromStarterApp() + => AgentMcpListStructuredLogsFromStarterAppCore(isolated: false, useDevLocalhost: false); + + [Fact] + [CaptureWorkspaceOnFailure] + public Task AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated() + => AgentMcpListStructuredLogsFromStarterAppCore(isolated: true, useDevLocalhost: false); + + [Fact] + [CaptureWorkspaceOnFailure] + public Task AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost() + => AgentMcpListStructuredLogsFromStarterAppCore(isolated: false, useDevLocalhost: true); + + private async Task AgentMcpListStructuredLogsFromStarterAppCore(bool isolated, bool useDevLocalhost) + { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var strategy = CliInstallStrategy.Detect(output.WriteLine); + + using var workspace = TemporaryWorkspace.Create(output); + + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); + + var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); + + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliAsync(strategy, counter); + + // Create a new Starter project (includes an ASP.NET Core apiservice) + await auto.AspireNewAsync("AspireMcpLogsApp", counter, useDevLocalhost: useDevLocalhost); + + // Navigate to the AppHost directory + await auto.TypeAsync("cd AspireMcpLogsApp/AspireMcpLogsApp.AppHost"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Start the AppHost + await auto.AspireStartAsync(counter, isolated: isolated); + + // Wait for the apiservice resource to be running before querying logs + await auto.TypeAsync("aspire wait apiservice --status up --timeout 300"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("is up (running).", timeout: TimeSpan.FromMinutes(6)); + await auto.WaitForSuccessPromptAsync(counter); + + // Call the MCP tool against the running AppHost + await auto.CallAgentMcpToolAsync(counter, "list_structured_logs", "STRUCTURED LOGS DATA"); + + // Stop the AppHost + await auto.AspireStopAsync(counter); + + // Exit the shell + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + + await pendingRun; + } +} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DashboardOtelTracesTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DashboardOtelTracesTests.cs deleted file mode 100644 index df23367956e..00000000000 --- a/tests/Aspire.Cli.EndToEnd.Tests/DashboardOtelTracesTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// 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.EndToEnd.Tests.Helpers; -using Aspire.Cli.Tests.Utils; -using Hex1b.Automation; -using Xunit; - -namespace Aspire.Cli.EndToEnd.Tests; - -/// -/// End-to-end tests for the aspire dashboard run and aspire otel traces commands. -/// Each test class runs as a separate CI job for parallelization. -/// -public sealed class DashboardOtelTracesTests(ITestOutputHelper output) -{ - [Fact] - [CaptureWorkspaceOnFailure] - public async Task DashboardRunWithOtelTracesReturnsNoTraces() - { - var repoRoot = CliE2ETestHelpers.GetRepoRoot(); - var strategy = CliInstallStrategy.Detect(output.WriteLine); - - var workspace = TemporaryWorkspace.Create(output); - - using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - - var counter = new SequenceCounter(); - var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - - await auto.PrepareDockerEnvironmentAsync(counter, workspace); - await auto.InstallAspireCliAsync(strategy, counter); - - // Store the dashboard log path inside the workspace so it gets captured on failure - var dashboardLogPath = $"/workspace/{workspace.WorkspaceRoot.Name}/dashboard.log"; - - // Start the dashboard in the background with anonymous access and telemetry API enabled - await auto.TypeAsync($"aspire dashboard run --allow-anonymous --enable-api > {dashboardLogPath} 2>&1 &"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - // Store the dashboard PID for cleanup - await auto.TypeAsync("DASHBOARD_PID=$!"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - // Wait for the dashboard to become ready by polling the frontend URL - await auto.TypeAsync("for i in $(seq 1 30); do curl -ksSL -o /dev/null -w '%{http_code}' http://localhost:18888 2>/dev/null | grep -q 200 && break; sleep 1; done"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); - - // Dump dashboard log for debugging visibility in the recording - await auto.TypeAsync($"echo '=== DASHBOARD LOG ==='; cat {dashboardLogPath}; echo '=== END DASHBOARD LOG ==='"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - // Dump CLI logs for debugging - await auto.TypeAsync("echo '=== CLI LOGS ==='; ls -lt ~/.aspire/logs/ 2>/dev/null; CLI_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | head -1); [ -n \"$CLI_LOG\" ] && tail -50 \"$CLI_LOG\"; echo '=== END CLI LOGS ==='"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - // Verify the dashboard process is still running before querying traces - await auto.TypeAsync("kill -0 $DASHBOARD_PID 2>/dev/null && echo 'dashboard-running' || echo 'dashboard-stopped'"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("dashboard-running", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptAsync(counter); - - // Run aspire otel traces against the standalone dashboard - await auto.TypeAsync("aspire otel traces --dashboard-url http://localhost:18888"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("No traces found", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitForSuccessPromptAsync(counter); - - // Clean up: kill the background dashboard process - await auto.TypeAsync("kill -9 $DASHBOARD_PID 2>/dev/null; wait $DASHBOARD_PID 2>/dev/null; true"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; - } -} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs new file mode 100644 index 00000000000..952078bd794 --- /dev/null +++ b/tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs @@ -0,0 +1,177 @@ +// 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.EndToEnd.Tests.Helpers; +using Aspire.Cli.Tests.Utils; +using Hex1b.Automation; +using Xunit; + +namespace Aspire.Cli.EndToEnd.Tests; + +/// +/// End-to-end tests for the aspire dashboard run command combined with aspire otel and aspire agent mcp. +/// Each test class runs as a separate CI job for parallelization. +/// +public sealed class DashboardRunTests(ITestOutputHelper output) +{ + [Fact] + [CaptureWorkspaceOnFailure] + public async Task DashboardRunWithOtelTracesReturnsNoTraces() + { + await DashboardRunWithOtelTracesReturnsNoTracesCore("http://localhost:18888", "http://localhost:18888"); + } + + [Fact] + [CaptureWorkspaceOnFailure] + public async Task DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost() + { + await DashboardRunWithOtelTracesReturnsNoTracesCore("http://dashboard.dev.localhost:18888", "http://localhost:18888"); + } + + private async Task DashboardRunWithOtelTracesReturnsNoTracesCore(string frontendUrl, string localhostUrl) + { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var strategy = CliInstallStrategy.Detect(output.WriteLine); + + var workspace = TemporaryWorkspace.Create(output); + + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); + + var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); + + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliAsync(strategy, counter); + + // Use a known browser token so we can construct the dashboard URL directly, + // avoiding the need to parse it from logs (which contain Spectre Console OSC 8 + // escape sequences that break grep-based URL extraction). + var browserToken = "testtoken1234567890abcdef12345678"; + + // Set the browser token env var before starting the dashboard + await auto.TypeAsync($"export DASHBOARD__FRONTEND__BROWSERTOKEN={browserToken}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Store the dashboard log path inside the workspace so it gets captured on failure + var dashboardLogPath = $"/workspace/{workspace.WorkspaceRoot.Name}/dashboard.log"; + + // Start the dashboard in the background with the specified frontend URL + await auto.TypeAsync($"aspire dashboard run --frontend-url {frontendUrl} > {dashboardLogPath} 2>&1 &"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Store the dashboard PID for cleanup + await auto.TypeAsync("DASHBOARD_PID=$!"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Wait for the dashboard to become ready by polling the localhost URL + await auto.TypeAsync($"for i in $(seq 1 30); do curl -ksSL -o /dev/null -w '%{{http_code}}' {localhostUrl} 2>/dev/null | grep -q 200 && break; sleep 1; done"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + // Dump dashboard log for debugging visibility in the recording + await auto.TypeAsync($"echo '=== DASHBOARD LOG ==='; cat {dashboardLogPath}; echo '=== END DASHBOARD LOG ==='"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Dump CLI logs for debugging + await auto.TypeAsync("echo '=== CLI LOGS ==='; ls -lt ~/.aspire/logs/ 2>/dev/null; CLI_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | head -1); [ -n \"$CLI_LOG\" ] && tail -50 \"$CLI_LOG\"; echo '=== END CLI LOGS ==='"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Construct the dashboard URL using the known token and the frontend URL. + // In the dev.localhost variant this exercises the CLI's NormalizeDashboardUrl path end-to-end. + var dashboardUrl = $"{frontendUrl}/login?t={browserToken}"; + + await auto.TypeAsync($"aspire otel traces --dashboard-url \"{dashboardUrl}\""); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("No traces found", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitForSuccessPromptAsync(counter); + + // Clean up: kill the background dashboard process + await auto.TypeAsync("kill -9 $DASHBOARD_PID 2>/dev/null; wait $DASHBOARD_PID 2>/dev/null; true"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Exit the shell + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + + await pendingRun; + } + + [Fact] + [CaptureWorkspaceOnFailure] + public Task DashboardRunWithAgentMcpListTracesReturnsNoTraces() + => DashboardRunWithAgentMcpCore("http://localhost:18888", "http://localhost:18888", "list_traces", "TRACES DATA"); + + [Fact] + [CaptureWorkspaceOnFailure] + public Task DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost() + => DashboardRunWithAgentMcpCore("http://dashboard.dev.localhost:18888", "http://localhost:18888", "list_traces", "TRACES DATA"); + + private async Task DashboardRunWithAgentMcpCore(string frontendUrl, string localhostUrl, string toolName, string expectedMarker) + { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var strategy = CliInstallStrategy.Detect(output.WriteLine); + + var workspace = TemporaryWorkspace.Create(output); + + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); + + var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); + + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliAsync(strategy, counter); + + // Use a known browser token so we can construct the dashboard URL directly + var browserToken = "testtoken1234567890abcdef12345678"; + + // Set the browser token env var before starting the dashboard + await auto.TypeAsync($"export DASHBOARD__FRONTEND__BROWSERTOKEN={browserToken}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Store the dashboard log path inside the workspace so it gets captured on failure + var dashboardLogPath = $"/workspace/{workspace.WorkspaceRoot.Name}/dashboard.log"; + + // Start the dashboard in the background with the specified frontend URL + await auto.TypeAsync($"aspire dashboard run --frontend-url {frontendUrl} > {dashboardLogPath} 2>&1 &"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Store the dashboard PID for cleanup + await auto.TypeAsync("DASHBOARD_PID=$!"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Wait for the dashboard to become ready by polling the localhost URL + await auto.TypeAsync($"for i in $(seq 1 30); do curl -ksSL -o /dev/null -w '%{{http_code}}' {localhostUrl} 2>/dev/null | grep -q 200 && break; sleep 1; done"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + // Construct the dashboard URL using the known token and the frontend URL + var dashboardUrl = $"{frontendUrl}/login?t={browserToken}"; + + // Call the MCP tool against the standalone dashboard + await auto.CallAgentMcpToolAsync(counter, toolName, expectedMarker, $"--dashboard-url \"{dashboardUrl}\""); + + // Clean up: kill the background dashboard process + await auto.TypeAsync("kill -9 $DASHBOARD_PID 2>/dev/null; wait $DASHBOARD_PID 2>/dev/null; true"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Exit the shell + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + + await pendingRun; + } +} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs index 8a69e1359d7..a2f33440763 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs @@ -580,7 +580,7 @@ await auto.TypeAsync( await auto.TypeAsync( $"DASHBOARD_URL=$(sed -n " + - "'s/.*\"dashboardUrl\"[[:space:]]*:[[:space:]]*\"\\(https\\?:\\/\\/localhost:[0-9]*\\).*/\\1/p' " + + "'s/.*\"dashboardUrl\"[[:space:]]*:[[:space:]]*\"\\(https\\?:\\/\\/[a-z.]*localhost:[0-9]*\\).*/\\1/p' " + $"\"{jsonFile}\" | head -1)"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); @@ -825,6 +825,59 @@ private static string BuildAspireDiagnosticsCaptureCommand(string destinationExp return null; } + /// + /// Sends a JSON-RPC initialize/initialized/tools-call sequence to aspire agent mcp and verifies the response. + /// + /// The terminal automator. + /// The prompt sequence counter. + /// The MCP tool name to invoke (e.g. list_structured_logs). + /// A string expected in the tool call output (e.g. STRUCTURED LOGS DATA). + /// Additional arguments to pass to aspire agent mcp (e.g. --dashboard-url "..."). + internal static async Task CallAgentMcpToolAsync( + this Hex1bTerminalAutomator auto, + SequenceCounter counter, + string toolName, + string expectedMarker, + string? mcpArgs = null) + { + var argsFragment = mcpArgs is not null ? $" {mcpArgs}" : string.Empty; + + // Send JSON-RPC messages to the MCP server via a compound command. + // The sleeps ensure proper protocol timing between initialize, initialized notification, and tool call. + await auto.TypeAsync( + "{ " + + "echo '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"e2e-test\",\"version\":\"0.1.0\"}}}'; " + + "sleep 3; " + + "echo '{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}'; " + + "sleep 1; " + + $"echo '{{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{{\"name\":\"{toolName}\",\"arguments\":{{}}}}}}'; " + + "sleep 15; " + + $"}} | aspire agent mcp{argsFragment} > /tmp/mcp_out.txt 2>/tmp/mcp_err.txt || true"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); + + // Dump output for debugging visibility in the recording + await auto.TypeAsync("cat /tmp/mcp_out.txt | head -50"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Check that the response contains the expected data marker + await auto.TypeAsync( + $"if grep -q '{expectedMarker}' /tmp/mcp_out.txt; then echo 'MCP_DATA_PRESENT'; " + + $"elif grep -q '{toolName}' /tmp/mcp_out.txt; then echo 'MCP_TOOL_FOUND_BUT_NO_DATA'; " + + "else echo 'MCP_DATA_MISSING'; fi"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("MCP_DATA_PRESENT", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForAnyPromptAsync(counter); + + // Verify the initialize response was received (confirms MCP handshake worked) + await auto.TypeAsync( + "grep -q 'aspire-mcp-server' /tmp/mcp_out.txt && echo 'MCP_INIT_OK' || echo 'MCP_INIT_MISSING'"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("MCP_INIT_OK", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForAnyPromptAsync(counter); + } + private static bool ShouldPreserveLocalWorkspace() { return TestContext.Current?.KeyValueStorage.TryGetValue("PreserveWorkspaceOnFailure", out var value) == true && diff --git a/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs index 119178a649d..6d19ae0f338 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs @@ -17,14 +17,19 @@ public sealed class OtelLogsTests(ITestOutputHelper output) [Fact] [CaptureWorkspaceOnFailure] public Task OtelLogsReturnsStructuredLogsFromStarterApp() - => OtelLogsReturnsStructuredLogsFromStarterAppCore(isolated: false); + => OtelLogsReturnsStructuredLogsFromStarterAppCore(isolated: false, useDevLocalhost: false); [Fact] [CaptureWorkspaceOnFailure] - public Task OtelLogsReturnsStructuredLogsFromStarterAppIsolated() - => OtelLogsReturnsStructuredLogsFromStarterAppCore(isolated: true); + public Task OtelLogsReturnsStructuredLogsFromStarterApp_Isolated() + => OtelLogsReturnsStructuredLogsFromStarterAppCore(isolated: true, useDevLocalhost: false); - private async Task OtelLogsReturnsStructuredLogsFromStarterAppCore(bool isolated) + [Fact] + [CaptureWorkspaceOnFailure] + public Task OtelLogsReturnsStructuredLogsFromStarterApp_DevLocalhost() + => OtelLogsReturnsStructuredLogsFromStarterAppCore(isolated: false, useDevLocalhost: true); + + private async Task OtelLogsReturnsStructuredLogsFromStarterAppCore(bool isolated, bool useDevLocalhost) { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); @@ -42,14 +47,14 @@ private async Task OtelLogsReturnsStructuredLogsFromStarterAppCore(bool isolated await auto.InstallAspireCliAsync(strategy, counter); // Create a new Starter project (includes an ASP.NET Core apiservice) - await auto.AspireNewAsync("AspireOtelLogsApp", counter); + await auto.AspireNewAsync("AspireOtelLogsApp", counter, useDevLocalhost: useDevLocalhost); // Navigate to the AppHost directory await auto.TypeAsync("cd AspireOtelLogsApp/AspireOtelLogsApp.AppHost"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - // Start the AppHost in the background + // Start the AppHost await auto.AspireStartAsync(counter, isolated: isolated); // Wait for the apiservice resource to be running before querying logs diff --git a/tests/Aspire.Cli.Tests/Commands/TelemetryLogsCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/TelemetryLogsCommandTests.cs index 461d5648b95..c3b9a5aad24 100644 --- a/tests/Aspire.Cli.Tests/Commands/TelemetryLogsCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/TelemetryLogsCommandTests.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Net; using System.Text.Json; +using System.Text.Json.Nodes; +using Aspire.Cli.Backchannel; using Aspire.Cli.Commands; using Aspire.Cli.Resources; using Aspire.Cli.Tests.TestServices; @@ -34,6 +36,206 @@ public async Task TelemetryLogsCommand_WhenNoAppHostRunning_ReturnsSuccess() Assert.Equal(ExitCodeConstants.Success, exitCode); } + [Fact] + public async Task TelemetryLogsCommand_WithDevLocalhostDashboardApiUrl_UsesLocalhost() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var requestedHosts = new List(); + var resourcesJson = JsonSerializer.Serialize( + new ResourceInfoJson[] { new() { Name = "redis", InstanceId = null } }, + OtlpJsonSerializerContext.Default.ResourceInfoJsonArray); + + var resourceLogs = new OtlpResourceLogsJson[] + { + new() + { + Resource = TelemetryTestHelper.CreateOtlpResource("redis", null), + ScopeLogs = + [ + new OtlpScopeLogsJson + { + LogRecords = + [ + new OtlpLogRecordJson + { + TimeUnixNano = TelemetryTestHelper.DateTimeToUnixNanoseconds(s_testTime), + SeverityNumber = 9, + SeverityText = "Information", + Body = new OtlpAnyValueJson { StringValue = "Ready to accept connections" }, + Attributes = + [ + new OtlpKeyValueJson { Key = "aspire.log_id", Value = new OtlpAnyValueJson { StringValue = "7" } } + ] + } + ] + } + ] + } + }; + var logsResponse = new TelemetryApiResponse + { + Data = new OtlpTelemetryDataJson { ResourceLogs = resourceLogs }, + TotalCount = 1, + ReturnedCount = 1 + }; + var logsJson = JsonSerializer.Serialize(logsResponse, OtlpJsonSerializerContext.Default.TelemetryApiResponse); + + var monitor = new TestAuxiliaryBackchannelMonitor(); + var connection = new TestAppHostAuxiliaryBackchannel + { + IsInScope = true, + AppHostInfo = new AppHostInformation + { + AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "TestAppHost", "TestAppHost.csproj"), + ProcessId = 1234 + }, + DashboardInfoResponse = new GetDashboardInfoResponse + { + ApiBaseUrl = "https://nextapp1.dev.localhost:64876", + ApiToken = "test-token", + DashboardUrls = ["https://nextapp1.dev.localhost:64876/login?t=test"], + IsHealthy = true + } + }; + monitor.AddConnection("hash1", "socket.hash1", connection); + + var handler = new MockHttpMessageHandler(request => + { + requestedHosts.Add(request.RequestUri!.Host); + + return request.RequestUri.AbsolutePath switch + { + "/api/telemetry/resources" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(resourcesJson, System.Text.Encoding.UTF8, "application/json") + }, + "/api/telemetry/logs" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(logsJson, System.Text.Encoding.UTF8, "application/json") + }, + _ => new HttpResponseMessage(HttpStatusCode.NotFound) + }; + }); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.AuxiliaryBackchannelMonitorFactory = _ => monitor; + options.OutputTextWriter = outputWriter; + options.DisableAnsi = true; + }); + services.AddSingleton(handler); + services.Replace(ServiceDescriptor.Singleton(new MockHttpClientFactory(handler))); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("otel logs --format json -n 5"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.All(requestedHosts, host => Assert.Equal("localhost", host)); + + var jsonLine = outputWriter.Logs.Single(l => l.TrimStart().StartsWith('[')); + var items = JsonNode.Parse(jsonLine)!.AsArray(); + Assert.Single(items); + + var item = items[0]!; + Assert.Equal("Ready to accept connections", item["message"]!.GetValue()); + Assert.Equal("redis", item["resourceName"]!.GetValue()); + + var dashboardUrl = item["dashboardUrl"]!.GetValue(); + Assert.Equal("https://nextapp1.dev.localhost:64876/structuredlogs?logEntryId=7", dashboardUrl); + } + + [Fact] + public async Task TelemetryLogsCommand_WithDevLocalhostDashboardUrlArg_PreservesDisplayUrl() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var requestedHosts = new List(); + + var resourceLogs = new OtlpResourceLogsJson[] + { + new() + { + Resource = TelemetryTestHelper.CreateOtlpResource("redis", null), + ScopeLogs = + [ + new OtlpScopeLogsJson + { + LogRecords = + [ + new OtlpLogRecordJson + { + TimeUnixNano = TelemetryTestHelper.DateTimeToUnixNanoseconds(s_testTime), + SeverityNumber = 9, + SeverityText = "Information", + Body = new OtlpAnyValueJson { StringValue = "Ready to accept connections" }, + Attributes = + [ + new OtlpKeyValueJson { Key = "aspire.log_id", Value = new OtlpAnyValueJson { StringValue = "7" } } + ] + } + ] + } + ] + } + }; + var logsResponse = new TelemetryApiResponse + { + Data = new OtlpTelemetryDataJson { ResourceLogs = resourceLogs }, + TotalCount = 1, + ReturnedCount = 1 + }; + var logsJson = JsonSerializer.Serialize(logsResponse, OtlpJsonSerializerContext.Default.TelemetryApiResponse); + + var handler = new MockHttpMessageHandler(request => + { + requestedHosts.Add(request.RequestUri!.Host); + + return request.RequestUri.AbsolutePath switch + { + "/api/telemetry/resources" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("[]", System.Text.Encoding.UTF8, "application/json") + }, + "/api/telemetry/logs" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(logsJson, System.Text.Encoding.UTF8, "application/json") + }, + _ => new HttpResponseMessage(HttpStatusCode.NotFound) + }; + }); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.OutputTextWriter = outputWriter; + options.DisableAnsi = true; + }); + services.AddSingleton(handler); + services.Replace(ServiceDescriptor.Singleton(new MockHttpClientFactory(handler))); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("otel logs --format json -n 5 --dashboard-url http://dashboard.dev.localhost:18888"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + + // HTTP requests should be normalized to localhost + Assert.All(requestedHosts, host => Assert.Equal("localhost", host)); + + var jsonLine = outputWriter.Logs.Single(l => l.TrimStart().StartsWith('[')); + var items = JsonNode.Parse(jsonLine)!.AsArray(); + Assert.Single(items); + + // The display URL in JSON output should preserve the original *.dev.localhost hostname + var dashboardUrl = items[0]!["dashboardUrl"]!.GetValue(); + Assert.Equal("http://dashboard.dev.localhost:18888/structuredlogs?logEntryId=7", dashboardUrl); + } + [Theory] [InlineData(-1)] [InlineData(0)] @@ -667,4 +869,36 @@ public async Task TelemetryLogsCommand_JsonOutput_ProducesExpectedJson() Assert.Equal(expected, formattedJson, ignoreLineEndingDifferences: true); } + + [Fact] + public async Task TelemetryLogsCommand_WithDevLocalhostUrl_ConnectionRefused_ErrorMessageShowsOriginalUrl() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var testInteractionService = new TestInteractionService(); + + var handler = new MockHttpMessageHandler(request => + { + throw new HttpRequestException("Connection refused"); + }); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => testInteractionService; + }); + services.AddSingleton(handler); + services.Replace(ServiceDescriptor.Singleton(new MockHttpClientFactory(handler))); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("otel logs --dashboard-url http://dashboard.dev.localhost:18888/login?t=sometoken"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.DashboardFailure, exitCode); + var errorMessage = Assert.Single(testInteractionService.DisplayedErrors); + // The error message should show the original *.dev.localhost URL the user typed, + // not the normalized localhost URL used for HTTP requests. + Assert.Equal(string.Format(CultureInfo.CurrentCulture, TelemetryCommandStrings.DashboardConnectionFailed, "http://dashboard.dev.localhost:18888"), errorMessage); + } } diff --git a/tests/Aspire.Cli.Tests/Commands/TelemetryTracesCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/TelemetryTracesCommandTests.cs index 1d43a010c55..6fd1cab30de 100644 --- a/tests/Aspire.Cli.Tests/Commands/TelemetryTracesCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/TelemetryTracesCommandTests.cs @@ -3,6 +3,8 @@ using System.Net; using System.Text.Json; +using System.Text.Json.Nodes; +using Aspire.Cli.Backchannel; using Aspire.Cli.Commands; using Aspire.Cli.Resources; using Aspire.Cli.Tests.TestServices; @@ -33,6 +35,201 @@ public async Task TelemetryTracesCommand_WhenNoAppHostRunning_ReturnsSuccess() Assert.Equal(ExitCodeConstants.Success, exitCode); } + [Fact] + public async Task TelemetryTracesCommand_WithDevLocalhostDashboardApiUrl_UsesLocalhost() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var requestedHosts = new List(); + var resourcesJson = JsonSerializer.Serialize( + new ResourceInfoJson[] { new() { Name = "frontend", InstanceId = null } }, + OtlpJsonSerializerContext.Default.ResourceInfoJsonArray); + + var resourceSpans = new OtlpResourceSpansJson[] + { + new() + { + Resource = TelemetryTestHelper.CreateOtlpResource("frontend", null), + ScopeSpans = + [ + new OtlpScopeSpansJson + { + Spans = + [ + new OtlpSpanJson + { + TraceId = "abc1234567890def", + SpanId = "span001", + Name = "GET /home", + StartTimeUnixNano = TelemetryTestHelper.DateTimeToUnixNanoseconds(s_testTime), + EndTimeUnixNano = TelemetryTestHelper.DateTimeToUnixNanoseconds(s_testTime.AddMilliseconds(50)), + Status = new OtlpSpanStatusJson { Code = 1 } + } + ] + } + ] + } + }; + var tracesResponse = new TelemetryApiResponse + { + Data = new OtlpTelemetryDataJson { ResourceSpans = resourceSpans }, + TotalCount = 1, + ReturnedCount = 1 + }; + var tracesJson = JsonSerializer.Serialize(tracesResponse, OtlpJsonSerializerContext.Default.TelemetryApiResponse); + + var monitor = new TestAuxiliaryBackchannelMonitor(); + var connection = new TestAppHostAuxiliaryBackchannel + { + IsInScope = true, + AppHostInfo = new AppHostInformation + { + AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "TestAppHost", "TestAppHost.csproj"), + ProcessId = 1234 + }, + DashboardInfoResponse = new GetDashboardInfoResponse + { + ApiBaseUrl = "https://nextapp1.dev.localhost:64876", + ApiToken = "test-token", + DashboardUrls = ["https://nextapp1.dev.localhost:64876/login?t=test"], + IsHealthy = true + } + }; + monitor.AddConnection("hash1", "socket.hash1", connection); + + var handler = new MockHttpMessageHandler(request => + { + requestedHosts.Add(request.RequestUri!.Host); + + return request.RequestUri.AbsolutePath switch + { + "/api/telemetry/resources" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(resourcesJson, System.Text.Encoding.UTF8, "application/json") + }, + "/api/telemetry/traces" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(tracesJson, System.Text.Encoding.UTF8, "application/json") + }, + _ => new HttpResponseMessage(HttpStatusCode.NotFound) + }; + }); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.AuxiliaryBackchannelMonitorFactory = _ => monitor; + options.OutputTextWriter = outputWriter; + options.DisableAnsi = true; + }); + services.AddSingleton(handler); + services.Replace(ServiceDescriptor.Singleton(new MockHttpClientFactory(handler))); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("otel traces --format json -n 5"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.All(requestedHosts, host => Assert.Equal("localhost", host)); + + var jsonLine = outputWriter.Logs.Single(l => l.TrimStart().StartsWith('[')); + var items = JsonNode.Parse(jsonLine)!.AsArray(); + Assert.Single(items); + + var item = items[0]!; + Assert.Equal("abc1234567890def", item["traceId"]!.GetValue()); + + var dashboardUrl = item["dashboardUrl"]!.GetValue(); + Assert.Equal("https://nextapp1.dev.localhost:64876/traces/detail/abc1234567890def", dashboardUrl); + } + + [Fact] + public async Task TelemetryTracesCommand_WithDevLocalhostDashboardUrlArg_PreservesDisplayUrl() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var requestedHosts = new List(); + + var resourceSpans = new OtlpResourceSpansJson[] + { + new() + { + Resource = TelemetryTestHelper.CreateOtlpResource("frontend", null), + ScopeSpans = + [ + new OtlpScopeSpansJson + { + Spans = + [ + new OtlpSpanJson + { + TraceId = "abc1234567890def", + SpanId = "span001", + Name = "GET /home", + StartTimeUnixNano = TelemetryTestHelper.DateTimeToUnixNanoseconds(s_testTime), + EndTimeUnixNano = TelemetryTestHelper.DateTimeToUnixNanoseconds(s_testTime.AddMilliseconds(50)), + Status = new OtlpSpanStatusJson { Code = 1 } + } + ] + } + ] + } + }; + var tracesResponse = new TelemetryApiResponse + { + Data = new OtlpTelemetryDataJson { ResourceSpans = resourceSpans }, + TotalCount = 1, + ReturnedCount = 1 + }; + var tracesJson = JsonSerializer.Serialize(tracesResponse, OtlpJsonSerializerContext.Default.TelemetryApiResponse); + + var handler = new MockHttpMessageHandler(request => + { + requestedHosts.Add(request.RequestUri!.Host); + + return request.RequestUri.AbsolutePath switch + { + "/api/telemetry/resources" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("[]", System.Text.Encoding.UTF8, "application/json") + }, + "/api/telemetry/traces" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(tracesJson, System.Text.Encoding.UTF8, "application/json") + }, + _ => new HttpResponseMessage(HttpStatusCode.NotFound) + }; + }); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.OutputTextWriter = outputWriter; + options.DisableAnsi = true; + }); + services.AddSingleton(handler); + services.Replace(ServiceDescriptor.Singleton(new MockHttpClientFactory(handler))); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("otel traces --format json -n 5 --dashboard-url http://dashboard.dev.localhost:18888"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + + // HTTP requests should be normalized to localhost + Assert.All(requestedHosts, host => Assert.Equal("localhost", host)); + + var jsonLine = outputWriter.Logs.Single(l => l.TrimStart().StartsWith('[')); + var items = JsonNode.Parse(jsonLine)!.AsArray(); + Assert.Single(items); + + // The display URL in JSON output should preserve the original *.dev.localhost hostname + var dashboardUrl = items[0]!["dashboardUrl"]!.GetValue(); + Assert.Equal("http://dashboard.dev.localhost:18888/traces/detail/abc1234567890def", dashboardUrl); + } + [Theory] [InlineData(-1)] [InlineData(0)] diff --git a/tests/Shared/Hex1bAutomatorTestHelpers.cs b/tests/Shared/Hex1bAutomatorTestHelpers.cs index ece616d1e01..524e34c9aee 100644 --- a/tests/Shared/Hex1bAutomatorTestHelpers.cs +++ b/tests/Shared/Hex1bAutomatorTestHelpers.cs @@ -502,7 +502,8 @@ internal static async Task AspireNewAsync( string projectName, SequenceCounter counter, AspireTemplate template = AspireTemplate.Starter, - bool useRedisCache = true) + bool useRedisCache = true, + bool useDevLocalhost = false) { var templateTimeout = TimeSpan.FromSeconds(60); @@ -607,7 +608,14 @@ await auto.WaitUntilAsync( s => new CellPatternSearcher().Find("Use *.dev.localhost URLs").Search(s).Count > 0, timeout: TimeSpan.FromSeconds(10), description: "URLs prompt"); - await auto.EnterAsync(); // Accept default "No" + if (useDevLocalhost) + { + await auto.TypeAsync("y"); + } + else + { + await auto.EnterAsync(); // Accept default "No" + } // Step 6: Redis prompt (only Starter, JsReact, PythonReact) if (template is AspireTemplate.Starter or AspireTemplate.JsReact or AspireTemplate.PythonReact) From f6c2b2aac1ec397f9993a9310f1c4c5483fdd6a0 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 5 May 2026 09:09:05 -0700 Subject: [PATCH 22/55] [release/13.3] Fix aspire init template install for non-stable CLI builds (#16654) (#16672) * [release/13.3] Fix aspire init template install for non-stable CLI builds (#16654) spire init ran `dotnet new install Aspire.ProjectTemplates@` with `nugetConfigFile: null` and `nugetSource: null`, bypassing the channel feed wiring used by `aspire new`. For non-stable CLI builds (staging/daily/PR), `Aspire.ProjectTemplates@` is only available on a per-commit darc feed (e.g. `darc-pub-microsoft-aspire-`), so install failed with exit code 103 in any C# repo containing a `.sln`. Extract the channel-aware template package resolution and install logic out of `DotNetTemplateFactory.ApplyTemplateAsync` and into `TemplateNuGetConfigService` as `ResolveTemplatePackageAsync` and `InstallTemplatePackageAsync`. Both `DotNetTemplateFactory` and `InitCommand` now consume the helper. The existing `aspire new` install path is preserved bit-for-bit (extraction is mechanical; `IncludePrHives: true` keeps PR-hive widening behavior). For `aspire init` this means: - The version sent to `dotnet new install` is now the channel-resolved one (e.g. `13.3.0`), not the raw `+sha` build metadata. - Init now honors the global `channel` configuration, matching `aspire new`. - On install failure, captured stdout/stderr is displayed before the error. - `ChannelNotFoundException` and `EmptyChoicesException` produce friendly errors instead of bubbling to the top-level "unexpected error" handler. - PR hives are intentionally NOT included in init's channel discovery so a developer with stale `~/.aspire/hives/*` doesn't get a different template than they'd get on a clean machine. Notes: - `TemplateNuGetConfigService` is a singleton; `IDotNetCliRunner` is transient and is therefore passed as a method parameter to `InstallTemplatePackageAsync` instead of being injected. - New regression tests cover: explicit channel passes the temp NuGet config, implicit channel leaves it null, PR hives don't widen init, and channel resolution failures produce friendly errors. Fixes #16654 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback (#16672) Multi-model code review caught the following issues: 1. Restore original order of operations in DotNetTemplateFactory.ApplyTemplateAsync. The first refactor moved extraArgsCallback ahead of template package resolution, which changed prompt/error precedence for `aspire new` (extra-args prompts like Redis-cache, test-framework, xUnit-version would now run before channel lookup, and answers would be discarded if resolution failed afterward). Restored the BEFORE order from release/13.3: ResolveTemplatePackageAsync first, then extraArgsCallback, then InstallTemplatePackageAsync. Updated the in-source comment to be accurate. 2. Catch NuGetPackageCacheException in InitCommand.DropCSharpProjectSkeletonAsync. The pre-extraction init code went straight to `dotnet new install` and never invoked a NuGet search, so feed search failures (offline, inaccessible feed, etc.) couldn't bubble up. After the extraction init now performs the search and was missing the catch, surfacing the failure as an unhandled "unexpected error". Added the catch with the same friendly-error treatment as ChannelNotFoundException / EmptyChoicesException. 3. Use TemplatingStrings.TemplateInstallationFailed in InitCommand for parity with `aspire new`. The previous ad-hoc string omitted the log file path, making post-mortem diagnosis harder. 4. Added a comment in InstallTemplatePackageAsync clarifying that the temporary NuGet config is intentionally disposed at the end of the install (only `dotnet new install` consumes it; the subsequent `dotnet new