From 941d1a19fecec515f7135a0dc72aa01b3d2f1145 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 08:43:00 -0700 Subject: [PATCH 01/30] [release/9.2] Cosmos, Redis and Postgres show keyvault when using key access or passwords with emulator (#8503) * Follow up keyvault changes - Pass the IKeyVaultSecretReference to the SecretResolver - Don't add the default keyvault when using the emulator. * Enhance Azure Key Vault integration and add access key authentication support for CosmosDB and other resources * Add the KeyVault resource, but remove it from the model in BeforeStart if the Azure resource is emulated or container in run mode. * Add Microsoft.Extensions.DependencyInjection using statement * Respond to PR feedback --------- Co-authored-by: David Fowler Co-authored-by: Eric Erhardt --- .../AzureCosmosDBExtensions.cs | 14 +++++++ .../AzureCosmosDBResource.cs | 2 +- .../AzureKeyVaultResource.cs | 4 +- .../AzureKeyVaultSecretReference.cs | 2 +- .../AzurePostgresExtensions.cs | 14 +++++++ .../AzureRedisExtensions.cs | 14 +++++++ src/Aspire.Hosting.Azure/IKeyVaultResource.cs | 2 +- .../Provisioners/BicepProvisioner.cs | 4 +- .../AzureBicepResourceTests.cs | 40 ++++++++++++++++--- .../AzureCosmosDBExtensionsTests.cs | 14 ++++++- .../AzurePostgresExtensionsTests.cs | 14 +++++++ .../AzureRedisExtensionsTests.cs | 14 +++++++ 12 files changed, 124 insertions(+), 14 deletions(-) diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index d0e027c8277..67c092d0413 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -342,6 +342,20 @@ public static IResourceBuilder WithAccessKeyAuthenticatio var kv = builder.ApplicationBuilder.AddAzureKeyVault($"{builder.Resource.Name}-kv") .WithParentRelationship(builder.Resource); + // remove the KeyVault from the model if the emulator is used during run mode. + // need to do this later in case builder becomes an emulator after this method is called. + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + builder.ApplicationBuilder.Eventing.Subscribe((data, _) => + { + if (builder.Resource.IsEmulator) + { + data.Model.Resources.Remove(kv.Resource); + } + return Task.CompletedTask; + }); + } + return builder.WithAccessKeyAuthentication(kv); } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs index eb9cb02fc62..b48179003e3 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs @@ -134,7 +134,7 @@ internal ReferenceExpression GetChildConnectionString(string childResourceName, var builder = new ReferenceExpressionBuilder(); - if (UseAccessKeyAuthentication) + if (UseAccessKeyAuthentication && !IsEmulator) { builder.AppendFormatted(ConnectionStringSecretOutput.Resource.GetSecretReference(GetKeyValueSecretName(childResourceName))); } diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs index bfae31c431e..88e441d402f 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs @@ -34,9 +34,9 @@ public class AzureKeyVaultResource(string name, Action VaultUri; // In run mode, this is set to the secret client used to access the Azure Key Vault. - internal Func>? SecretResolver { get; set; } + internal Func>? SecretResolver { get; set; } - Func>? IKeyVaultResource.SecretResolver + Func>? IKeyVaultResource.SecretResolver { get => SecretResolver; set => SecretResolver = value; diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs index 3f078529b32..b5579afd20e 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs @@ -28,7 +28,7 @@ internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVa { if (azureKeyVaultResource.SecretResolver is { } secretResolver) { - return await secretResolver(secretName, cancellationToken).ConfigureAwait(false); + return await secretResolver(this, cancellationToken).ConfigureAwait(false); } throw new InvalidOperationException($"Secret '{secretName}' not found in Key Vault '{azureKeyVaultResource.Name}'."); diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs index 59902d48d8f..7f147620a2b 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs @@ -289,6 +289,20 @@ public static IResourceBuilder WithPassword var kv = builder.ApplicationBuilder.AddAzureKeyVault($"{builder.Resource.Name}-kv") .WithParentRelationship(builder.Resource); + // remove the KeyVault from the model if the emulator is used during run mode. + // need to do this later in case builder becomes an emulator after this method is called. + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + builder.ApplicationBuilder.Eventing.Subscribe((data, token) => + { + if (builder.Resource.IsContainer()) + { + data.Model.Resources.Remove(kv.Resource); + } + return Task.CompletedTask; + }); + } + return builder.WithPasswordAuthentication(kv, userName, password); } diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs index 0795cebc879..219fb258009 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs @@ -195,6 +195,20 @@ public static IResourceBuilder WithAccessKeyAuthenticat var kv = builder.ApplicationBuilder.AddAzureKeyVault($"{builder.Resource.Name}-kv") .WithParentRelationship(builder.Resource); + // remove the KeyVault from the model if the emulator is used during run mode. + // need to do this later in case builder becomes an emulator after this method is called. + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + builder.ApplicationBuilder.Eventing.Subscribe((data, token) => + { + if (builder.Resource.IsContainer()) + { + data.Model.Resources.Remove(kv.Resource); + } + return Task.CompletedTask; + }); + } + return builder.WithAccessKeyAuthentication(kv); } diff --git a/src/Aspire.Hosting.Azure/IKeyVaultResource.cs b/src/Aspire.Hosting.Azure/IKeyVaultResource.cs index 9226de80447..4760e0e8368 100644 --- a/src/Aspire.Hosting.Azure/IKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure/IKeyVaultResource.cs @@ -23,7 +23,7 @@ public interface IKeyVaultResource : IResource, IAzureResource /// /// Gets or sets the secret resolver function used to resolve secrets at runtime. /// - Func>? SecretResolver { get; set; } + Func>? SecretResolver { get; set; } /// /// Gets a secret reference for the specified secret name. diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs index 12581f26de2..6e6b8598349 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs @@ -284,9 +284,9 @@ await notificationService.PublishUpdateAsync(resource, state => // Set the client for resolving secrets at runtime var client = new SecretClient(new(vaultUri), context.Credential); - kvr.SecretResolver = async (secretName, ct) => + kvr.SecretResolver = async (secretRef, ct) => { - var secret = await client.GetSecretAsync(secretName, cancellationToken: ct).ConfigureAwait(false); + var secret = await client.GetSecretAsync(secretRef.SecretName, cancellationToken: ct).ConfigureAwait(false); return secret.Value.Value; }; } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs index b25ccd148fe..5cd01f6b256 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; +using System.Runtime.CompilerServices; using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; @@ -237,6 +238,24 @@ public async Task AddAzureCosmosDBEmulator() Assert.Equal(cs, await ((IResourceWithConnectionString)cosmos.Resource).GetConnectionStringAsync()); } + [Fact] + public async Task AddAzureCosmosDB_WithAccessKeyAuthentication_NoKeyVaultWithEmulator() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddAzureCosmosDB("cosmos").WithAccessKeyAuthentication().RunAsEmulator(); + +#pragma warning disable ASPIRECOSMOSDB001 + builder.AddAzureCosmosDB("cosmos2").WithAccessKeyAuthentication().RunAsPreviewEmulator(); +#pragma warning restore ASPIRECOSMOSDB001 + + var app = builder.Build(); + var model = app.Services.GetRequiredService(); + await ExecuteBeforeStartHooksAsync(app, CancellationToken.None); + + Assert.Empty(model.Resources.OfType()); + } + [Theory] [InlineData(null)] [InlineData("mykeyvault")] @@ -264,16 +283,24 @@ public async Task AddAzureCosmosDBViaRunMode_WithAccessKeyAuthentication(string? var db = cosmos.AddCosmosDatabase("db", databaseName: "mydatabase"); db.AddContainer("container", "mypartitionkeypath", containerName: "mycontainer"); - var kv = builder.CreateResourceBuilder(kvName); + var app = builder.Build(); + + await ExecuteBeforeStartHooksAsync(app, CancellationToken.None); + + var model = app.Services.GetRequiredService(); + + var kv = model.Resources.OfType().Single(); + + Assert.Equal(kvName, kv.Name); var secrets = new Dictionary { ["connectionstrings--cosmos"] = "mycosmosconnectionstring" }; - kv.Resource.SecretResolver = (name, _) => + kv.SecretResolver = (secretRef, _) => { - if (!secrets.TryGetValue(name, out var value)) + if (!secrets.TryGetValue(secretRef.SecretName, out var value)) { return Task.FromResult(null); } @@ -533,9 +560,9 @@ public async Task AddAzureCosmosDBViaPublishMode_WithAccessKeyAuthentication(str ["connectionstrings--cosmos"] = "mycosmosconnectionstring" }; - kv.Resource.SecretResolver = (name, _) => + kv.Resource.SecretResolver = (secretRef, _) => { - if (!secrets.TryGetValue(name, out var value)) + if (!secrets.TryGetValue(secretRef.SecretName, out var value)) { return Task.FromResult(null); } @@ -3130,6 +3157,9 @@ public async Task InfrastructureCanBeMutatedAfterCreation() Assert.Equal(expectedBicep, bicep); } + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")] + private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken); + private sealed class ProjectA : IProjectMetadata { public string ProjectPath => "projectA"; diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs index 7f7e2d2e18a..4c5475efbec 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs @@ -106,16 +106,26 @@ public void AzureCosmosDBHasCorrectConnectionStrings_ForAccountEndpoint() Assert.Equal("AccountEndpoint={cosmos.outputs.connectionString};Database=db1;Container=container1", container1.Resource.ConnectionStringExpression.ValueExpression); } - [Fact] - public void AzureCosmosDBHasCorrectConnectionStrings() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AzureCosmosDBHasCorrectConnectionStrings(bool useAccessKeyAuth) { using var builder = TestDistributedApplicationBuilder.Create(); var cosmos = builder.AddAzureCosmosDB("cosmos").RunAsEmulator(); + if (useAccessKeyAuth) + { + cosmos.WithAccessKeyAuthentication(); + } var db1 = cosmos.AddCosmosDatabase("db1"); var container1 = db1.AddContainer("container1", "id"); var cosmos1 = builder.AddAzureCosmosDB("cosmos1").RunAsEmulator(); + if (useAccessKeyAuth) + { + cosmos1.WithAccessKeyAuthentication(); + } var db2 = cosmos1.AddCosmosDatabase("db2", "db"); var container2 = db2.AddContainer("container2", "id", "container"); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs index e8c90e020b6..cc011572937 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs @@ -145,6 +145,20 @@ param principalName string Assert.Equal(expectedBicep, postgresRolesManifest.BicepText); } + [Fact] + public async Task AddAzurePostgresFlexibleServer_WithPasswordAuthentication_NoKeyVaultWithContainer() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddAzurePostgresFlexibleServer("pg").WithPasswordAuthentication().RunAsContainer(); + + var app = builder.Build(); + var model = app.Services.GetRequiredService(); + await ExecuteBeforeStartHooksAsync(app, CancellationToken.None); + + Assert.Empty(model.Resources.OfType()); + } + [Theory] [InlineData(true, true, null)] [InlineData(true, true, "mykeyvault")] diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs index 53dfe5b9770..bc3d6481fda 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs @@ -99,6 +99,20 @@ param principalName string Assert.Equal(expectedBicep, redisRolesManifest.BicepText); } + [Fact] + public async Task AddAzureRedis_WithAccessKeyAuthentication_NoKeyVaultWithContainer() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddAzureRedis("redis").WithAccessKeyAuthentication().RunAsContainer(); + + var app = builder.Build(); + var model = app.Services.GetRequiredService(); + await ExecuteBeforeStartHooksAsync(app, CancellationToken.None); + + Assert.Empty(model.Resources.OfType()); + } + [Theory] [InlineData(null)] [InlineData("mykeyvault")] From b5a735e6ad8659da813a22382b84635cacbe6ed2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:11:44 -0700 Subject: [PATCH 02/30] [release/9.2] Interactive prompting for `aspire new` (#8523) * Tweak tracing code. * aspire new with prompting. * Template selection. --------- Co-authored-by: Mitch Denny --- .../Backchannel/AppHostBackchannel.cs | 16 +- src/Aspire.Cli/Commands/AddCommand.cs | 6 +- src/Aspire.Cli/Commands/NewCommand.cs | 142 ++++++++++-------- src/Aspire.Cli/Commands/PublishCommand.cs | 4 +- src/Aspire.Cli/Commands/RunCommand.cs | 4 +- src/Aspire.Cli/DotNetCliRunner.cs | 32 ++-- src/Aspire.Cli/NuGetPackageCache.cs | 10 +- src/Aspire.Cli/Program.cs | 30 ++-- src/Aspire.Cli/Utils/PromptUtils.cs | 45 ++++++ 9 files changed, 184 insertions(+), 105 deletions(-) create mode 100644 src/Aspire.Cli/Utils/PromptUtils.cs diff --git a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs index a61855bda5a..7558ea489b4 100644 --- a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs @@ -11,13 +11,13 @@ namespace Aspire.Cli.Backchannel; internal sealed class AppHostBackchannel(ILogger logger, CliRpcTarget target) { - private readonly ActivitySource _activitySource = new(nameof(Aspire.Cli.Backchannel.AppHostBackchannel), "1.0.0"); + private readonly ActivitySource _activitySource = new(nameof(AppHostBackchannel)); private readonly TaskCompletionSource _rpcTaskCompletionSource = new(); private Process? _process; public async Task PingAsync(long timestamp, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(PingAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -37,7 +37,7 @@ public async Task RequestStopAsync(CancellationToken cancellationToken) // of the AppHost process. The AppHost process will then trigger the shutdown // which will allow the CLI to await the pending run. - using var activity = _activitySource.StartActivity(nameof(RequestStopAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -51,7 +51,7 @@ await rpc.InvokeWithCancellationAsync( public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetDashboardUrlsAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -67,7 +67,7 @@ await rpc.InvokeWithCancellationAsync( public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetResourceStatesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -88,7 +88,7 @@ await rpc.InvokeWithCancellationAsync( public async Task ConnectAsync(Process process, string socketPath, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(ConnectAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); _process = process; @@ -111,7 +111,7 @@ public async Task ConnectAsync(Process process, string socketPath, CancellationT public async Task GetPublishersAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetPublishersAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false); @@ -127,7 +127,7 @@ public async Task GetPublishersAsync(CancellationToken cancellationTok public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetPublishingActivitiesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs index 23c4380db1c..03d9545df9e 100644 --- a/src/Aspire.Cli/Commands/AddCommand.cs +++ b/src/Aspire.Cli/Commands/AddCommand.cs @@ -11,7 +11,7 @@ namespace Aspire.Cli.Commands; internal sealed class AddCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(AddCommand)); private readonly DotNetCliRunner _runner; private readonly INuGetPackageCache _nuGetPackageCache; @@ -42,7 +42,7 @@ public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + using var activity = _activitySource.StartActivity(); try { @@ -62,7 +62,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var packages = await AnsiConsole.Status().StartAsync( "Searching for Aspire packages...", - context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile, prerelease, source, cancellationToken) + context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile.Directory!, prerelease, source, cancellationToken) ); var version = parseResult.GetValue("--version"); diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 7cbc4f104e1..f676383e29d 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -2,25 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; -using System.CommandLine.Parsing; using System.Diagnostics; using Aspire.Cli.Utils; +using Semver; using Spectre.Console; namespace Aspire.Cli.Commands; internal sealed class NewCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(NewCommand)); private readonly DotNetCliRunner _runner; + private readonly INuGetPackageCache _nuGetPackageCache; - public NewCommand(DotNetCliRunner runner) : base("new", "Create a new Aspire sample project.") + public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("new", "Create a new Aspire sample project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); + ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache)); _runner = runner; + _nuGetPackageCache = nuGetPackageCache; var templateArgument = new Argument("template"); - templateArgument.Validators.Add(ValidateProjectTemplate); templateArgument.Arity = ArgumentArity.ZeroOrOne; Arguments.Add(templateArgument); @@ -29,9 +31,6 @@ internal sealed class NewCommand : BaseCommand var outputOption = new Option("--output", "-o"); Options.Add(outputOption); - - var prereleaseOption = new Option("--prerelease"); - Options.Add(prereleaseOption); var sourceOption = new Option("--source", "-s"); Options.Add(sourceOption); @@ -40,7 +39,7 @@ internal sealed class NewCommand : BaseCommand Options.Add(templateVersionOption); } - private static void ValidateProjectTemplate(ArgumentResult result) + private static async Task<(string TemplateName, string TemplateDescription, string? PathAppendage)> GetProjectTemplateAsync(ParseResult parseResult, CancellationToken cancellationToken) { // TODO: We need to integrate with the template engine to interrogate // the list of available templates. For now we will just hard-code @@ -49,55 +48,91 @@ private static void ValidateProjectTemplate(ArgumentResult result) // Once we integrate with template engine we will also be able to // interrogate the various options and add them. For now we will // keep it simple. - string[] validTemplates = [ - "aspire-starter", - "aspire", - "aspire-apphost", - "aspire-servicedefaults", - "aspire-mstest", - "aspire-nunit", - "aspire-xunit" + (string TemplateName, string TemplateDescription, string? PathAppendage)[] validTemplates = [ + ("aspire-starter", "Aspire Starter App", "src") , + ("aspire", "Aspire Empty App", "src"), + ("aspire-apphost", "Aspire App Host", null), + ("aspire-servicedefaults", "Aspire Service Defaults", null), + ("aspire-mstest", "Aspire Test Project (MSTest)", null), + ("aspire-nunit", "Aspire Test Project (NUnit)", null), + ("aspire-xunit", "Aspire Test Project (xUnit)", null) ]; - var value = result.GetValueOrDefault(); - - if (value is null) + if (parseResult.GetValue("template") is { } templateName && validTemplates.SingleOrDefault(t => t.TemplateName == templateName) is { } template) { - // This is OK, for now we will use the default - // template of aspire-starter, but we might - // be able to do more intelligent selection in the - // future based on what is already in the working directory. - return; + return template; } - - if (value is { } templateName && !validTemplates.Contains(templateName)) + else { - result.AddError($"The specified template '{templateName}' is not valid. Valid templates are [{string.Join(", ", validTemplates)}]."); - return; + return await PromptUtils.PromptForSelectionAsync( + "Select a project template:", + validTemplates, + t => $"{t.TemplateName} ({t.TemplateDescription})", + cancellationToken + ); } } - protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + private static async Task GetProjectNameAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + if (parseResult.GetValue("--name") is not { } name) + { + var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name; + name = await PromptUtils.PromptForStringAsync("Enter the project name:", + defaultValue: defaultName, + cancellationToken: cancellationToken); + } - var templateVersion = parseResult.GetValue("--version"); - var prerelease = parseResult.GetValue("--prerelease"); + return name; + } - if (templateVersion is not null && prerelease) + private static async Task GetOutputPathAsync(ParseResult parseResult, string? pathAppendage, CancellationToken cancellationToken) + { + if (parseResult.GetValue("--output") is not { } outputPath) { - AnsiConsole.MarkupLine("[red bold]:thumbs_down: The --version and --prerelease options are mutually exclusive.[/]"); - return ExitCodeConstants.FailedToCreateNewProject; + outputPath = await PromptUtils.PromptForStringAsync( + "Enter the output path:", + defaultValue: Path.Combine(Environment.CurrentDirectory, pathAppendage ?? string.Empty), + cancellationToken: cancellationToken + ); } - else if (prerelease) + + return Path.GetFullPath(outputPath); + } + + private static async Task GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + if (parseResult.GetValue("--version") is { } version) { - templateVersion = "*-*"; + return version; } - else if (templateVersion is null) + else { - templateVersion = VersionHelper.GetDefaultTemplateVersion(); + version = await PromptUtils.PromptForStringAsync( + "Project templates version:", + defaultValue: VersionHelper.GetDefaultTemplateVersion(), + validator: (string value) => { + if (SemVersion.TryParse(value, out var parsedVersion)) + { + return ValidationResult.Success(); + } + + return ValidationResult.Error("Invalid version format. Please enter a valid version."); + }, + cancellationToken); + + return version; } + } + protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + using var activity = _activitySource.StartActivity(); + + var template = await GetProjectTemplateAsync(parseResult, cancellationToken); + var name = await GetProjectNameAsync(parseResult, cancellationToken); + var outputPath = await GetOutputPathAsync(parseResult, template.PathAppendage, cancellationToken); + var version = await GetProjectTemplatesVersionAsync(parseResult, cancellationToken); var source = parseResult.GetValue("--source"); var templateInstallResult = await AnsiConsole.Status() @@ -106,7 +141,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell .StartAsync( ":ice: Getting latest templates...", async context => { - return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", templateVersion!, source, true, cancellationToken); + return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken); }); if (templateInstallResult.ExitCode != 0) @@ -117,23 +152,6 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell AnsiConsole.MarkupLine($":package: Using project templates version: {templateInstallResult.TemplateVersion}"); - var templateName = parseResult.GetValue("template") ?? "aspire-starter"; - - if (parseResult.GetValue("--output") is not { } outputPath) - { - outputPath = Environment.CurrentDirectory; - } - else - { - outputPath = Path.GetFullPath(outputPath); - } - - if (parseResult.GetValue("--name") is not { } name) - { - var outputPathDirectoryInfo = new DirectoryInfo(outputPath); - name = outputPathDirectoryInfo.Name; - } - int newProjectExitCode = await AnsiConsole.Status() .Spinner(Spinner.Known.Dots3) .SpinnerStyle(Style.Parse("purple")) @@ -141,11 +159,11 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell ":rocket: Creating new Aspire project...", async context => { return await _runner.NewProjectAsync( - templateName, - name, - outputPath, - cancellationToken); - }); + template.TemplateName, + name, + outputPath, + cancellationToken); + }); if (newProjectExitCode != 0) { diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 496a282bf2f..f25893beb53 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -12,7 +12,7 @@ namespace Aspire.Cli.Commands; internal sealed class PublishCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(PublishCommand)); private readonly DotNetCliRunner _runner; public PublishCommand(DotNetCliRunner runner) : base("publish", "Generates deployment artifacts for an Aspire app host project.") @@ -34,7 +34,7 @@ internal sealed class PublishCommand : BaseCommand protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + using var activity = _activitySource.StartActivity(); var passedAppHostProjectFile = parseResult.GetValue("--project"); var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 566667fd8d1..f5067dffa53 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -14,7 +14,7 @@ namespace Aspire.Cli.Commands; internal sealed class RunCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(RunCommand)); private readonly DotNetCliRunner _runner; public RunCommand(DotNetCliRunner runner) : base("run", "Run an Aspire app host in development mode.") @@ -33,7 +33,7 @@ public RunCommand(DotNetCliRunner runner) : base("run", "Run an Aspire app host protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + using var activity = _activitySource.StartActivity(); var passedAppHostProjectFile = parseResult.GetValue("--project"); var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index ae8188cd700..40eafffa202 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -15,13 +15,13 @@ namespace Aspire.Cli; internal sealed class DotNetCliRunner(ILogger logger, IServiceProvider serviceProvider) { - private readonly ActivitySource _activitySource = new ActivitySource(nameof(Aspire.Cli.DotNetCliRunner)); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(DotNetCliRunner)); internal Func GetCurrentProcessId { get; set; } = () => Environment.ProcessId; public async Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(FileInfo projectFile, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetAppHostInformationAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["msbuild", "-getproperty:IsAspireHost,AspireHostingSDKVersion"]; @@ -79,7 +79,7 @@ internal sealed class DotNetCliRunner(ILogger logger, IServiceP public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(RunAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); if (watch && noBuild) { @@ -100,7 +100,7 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, public async Task CheckHttpCertificateAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(CheckHttpCertificateAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["dev-certs", "https", "--check", "--trust"]; return await ExecuteAsync( @@ -114,7 +114,7 @@ public async Task CheckHttpCertificateAsync(CancellationToken cancellationT public async Task TrustHttpCertificateAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(TrustHttpCertificateAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["dev-certs", "https", "--trust"]; return await ExecuteAsync( @@ -230,7 +230,7 @@ private static bool TryParsePackageVersionFromStdout(string stdout, [NotNullWhen public async Task NewProjectAsync(string templateName, string name, string outputPath, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(NewProjectAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["new", templateName, "--name", name, "--output", outputPath]; return await ExecuteAsync( @@ -259,7 +259,7 @@ internal static string GetBackchannelSocketPath() public async Task ExecuteAsync(string[] args, IDictionary? env, DirectoryInfo workingDirectory, TaskCompletionSource? backchannelCompletionSource, Action? streamsCallback, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(ExecuteAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var startInfo = new ProcessStartInfo("dotnet") { @@ -379,7 +379,7 @@ async Task ForwardStreamToLoggerAsync(StreamReader reader, string identifier, Pr private async Task StartBackchannelAsync(Process process, string socketPath, TaskCompletionSource backchannelCompletionSource, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(StartBackchannelAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50)); @@ -431,7 +431,7 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas public async Task BuildAsync(FileInfo projectFilePath, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(BuildAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["build", projectFilePath.FullName]; return await ExecuteAsync( @@ -444,7 +444,7 @@ public async Task BuildAsync(FileInfo projectFilePath, CancellationToken ca } public async Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(AddPackageAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = [ "add", @@ -477,9 +477,9 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN return result; } - public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(FileInfo projectFilePath, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken) + public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(SearchPackagesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); List cliArgs = [ "package", @@ -510,7 +510,7 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN var result = await ExecuteAsync( args: cliArgs.ToArray(), env: null, - workingDirectory: projectFilePath.Directory!, + workingDirectory: workingDirectory!, backchannelCompletionSource: null, streamsCallback: (_, output, _) => { // We need to read the output of the streams @@ -550,6 +550,12 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN foreach (var packageResult in sourcePackagesArray.EnumerateArray()) { var id = packageResult.GetProperty("id").GetString(); + + // var version = prerelease switch { + // true => packageResult.GetProperty("version").GetString(), + // false => packageResult.GetProperty("latestVersion").GetString() + // }; + var version = packageResult.GetProperty("latestVersion").GetString(); foundPackages.Add(new NuGetPackage diff --git a/src/Aspire.Cli/NuGetPackageCache.cs b/src/Aspire.Cli/NuGetPackageCache.cs index ef90821c873..402c99a9f92 100644 --- a/src/Aspire.Cli/NuGetPackageCache.cs +++ b/src/Aspire.Cli/NuGetPackageCache.cs @@ -8,18 +8,18 @@ namespace Aspire.Cli; internal interface INuGetPackageCache { - Task> GetPackagesAsync(FileInfo projectFile, bool prerelease, string? source, CancellationToken cancellationToken); + Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken); } internal sealed class NuGetPackageCache(ILogger logger, DotNetCliRunner cliRunner) : INuGetPackageCache { - private readonly ActivitySource _activitySource = new(nameof(Aspire.Cli.NuGetPackageCache), "1.0.0"); + private readonly ActivitySource _activitySource = new(nameof(NuGetPackageCache)); private const int SearchPageSize = 100; - public async Task> GetPackagesAsync(FileInfo projectFile, bool prerelease, string? source, CancellationToken cancellationToken) + public async Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetPackagesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); logger.LogDebug("Getting integrations from NuGet"); @@ -31,7 +31,7 @@ public async Task> GetPackagesAsync(FileInfo projectFi { // This search should pick up Aspire.Hosting.* and CommunityToolkit.Aspire.Hosting.* var result = await cliRunner.SearchPackagesAsync( - projectFile, + workingDirectory, "Aspire.Hosting", prerelease, SearchPageSize, diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index 37ec3f33636..9d86c0ee70a 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -10,13 +10,15 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using RootCommand = Aspire.Cli.Commands.RootCommand; namespace Aspire.Cli; public class Program { - private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(Aspire.Cli.Program)); + private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(Program)); private static IHost BuildApplication(string[] args) { @@ -30,14 +32,22 @@ private static IHost BuildApplication(string[] args) logging.IncludeScopes = true; }); - var otelBuilder = builder.Services.AddOpenTelemetry() - .WithTracing(tracing => { - tracing.AddSource( - nameof(Aspire.Cli.NuGetPackageCache), - nameof(Aspire.Cli.Backchannel.AppHostBackchannel), - nameof(Aspire.Cli.DotNetCliRunner), - nameof(Aspire.Cli.Program)); - }); + var otelBuilder = builder.Services + .AddOpenTelemetry() + .WithTracing(tracing => { + tracing.AddSource( + nameof(NuGetPackageCache), + nameof(AppHostBackchannel), + nameof(DotNetCliRunner), + nameof(Program), + nameof(NewCommand), + nameof(RunCommand), + nameof(AddCommand), + nameof(PublishCommand) + ); + + tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("aspire-cli")); + }); if (builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] is {}) { @@ -84,7 +94,7 @@ public static async Task Main(string[] args) var config = new CommandLineConfiguration(rootCommand); config.EnableDefaultExceptionHandler = true; - using var activity = s_activitySource.StartActivity(nameof(Main), ActivityKind.Internal); + using var activity = s_activitySource.StartActivity(); var exitCode = await config.InvokeAsync(args); await app.StopAsync().ConfigureAwait(false); diff --git a/src/Aspire.Cli/Utils/PromptUtils.cs b/src/Aspire.Cli/Utils/PromptUtils.cs new file mode 100644 index 00000000000..4b00b885e1d --- /dev/null +++ b/src/Aspire.Cli/Utils/PromptUtils.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Spectre.Console; + +namespace Aspire.Cli.Utils; + +internal static class PromptUtils +{ + public static async Task PromptForStringAsync(string promptText, string? defaultValue = null, Func? validator = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(promptText, nameof(promptText)); + var prompt = new TextPrompt(promptText); + + if (defaultValue is not null) + { + prompt.DefaultValue(defaultValue); + prompt.ShowDefaultValue(); + } + + if (validator is not null) + { + prompt.Validate(validator); + } + + return await AnsiConsole.PromptAsync(prompt, cancellationToken); + } + + public static async Task PromptForSelectionAsync(string promptText, IEnumerable choices, Func choiceFormatter, CancellationToken cancellationToken = default) where T: notnull + { + ArgumentNullException.ThrowIfNull(promptText, nameof(promptText)); + ArgumentNullException.ThrowIfNull(choices, nameof(choices)); + ArgumentNullException.ThrowIfNull(choiceFormatter, nameof(choiceFormatter)); + + var prompt = new SelectionPrompt() + .Title(promptText) + .UseConverter(choiceFormatter) + .AddChoices(choices) + .PageSize(10) + .EnableSearch() + .HighlightStyle(Style.Parse("darkmagenta")); + + return await AnsiConsole.PromptAsync(prompt, cancellationToken); + } +} \ No newline at end of file From 5013379f4b3bf984a7c963b3cead78c9686a5f16 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 3 Apr 2025 13:32:13 -0700 Subject: [PATCH 03/30] Add 3rd party signing information for OpenTelemetry and Semver libraries (#8527) --- eng/Signing.props | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/eng/Signing.props b/eng/Signing.props index 1d50eda172e..236a68adcbf 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -26,6 +26,12 @@ + + + + + + From a0bedeec28f2bc26f11caea7581981a78280ad5b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:30:33 -0700 Subject: [PATCH 04/30] Rename IKeyVaultResource and IKeyVaultSecretReference (#8537) Using the Azure prefix, as we do this everywhere else. Contributes to #7811 Co-authored-by: Eric Erhardt --- .../AzureContainerAppsInfrastructure.cs | 4 ++-- .../AzureCosmosDBExtensions.cs | 2 +- .../AzureCosmosDBResource.cs | 2 +- .../AzureKeyVaultResource.cs | 10 +++++----- .../AzureKeyVaultSecretReference.cs | 4 ++-- .../AzurePostgresExtensions.cs | 4 ++-- .../AzurePostgresFlexibleServerResource.cs | 2 +- .../AzureRedisCacheResource.cs | 2 +- src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs | 4 ++-- src/Aspire.Hosting.Azure/AzureResourcePreparer.cs | 2 +- ...{IKeyVaultResource.cs => IAzureKeyVaultResource.cs} | 6 +++--- ...etReference.cs => IAzureKeyVaultSecretReference.cs} | 6 +++--- .../Provisioning/Provisioners/BicepProvisioner.cs | 2 +- 13 files changed, 25 insertions(+), 25 deletions(-) rename src/Aspire.Hosting.Azure/{IKeyVaultResource.cs => IAzureKeyVaultResource.cs} (80%) rename src/Aspire.Hosting.Azure/{IKeyVaultSecretReference.cs => IAzureKeyVaultSecretReference.cs} (78%) diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs index 2f0a1a3ccbe..cfce5b42e64 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs @@ -714,7 +714,7 @@ BicepValue GetHostValue(string? prefix = null, string? suffix = null) return (AllocateParameter(secretOutputReference, secretType: SecretType.KeyVault), SecretType.KeyVault); } - if (value is IKeyVaultSecretReference vaultSecretReference) + if (value is IAzureKeyVaultSecretReference vaultSecretReference) { if (parent is null) { @@ -797,7 +797,7 @@ private BicepValue AllocateKeyVaultSecretUriReference(BicepSecretOutputR return secret.Properties.SecretUri; } - private BicepValue AllocateKeyVaultSecretUriReference(IKeyVaultSecretReference secretOutputReference) + private BicepValue AllocateKeyVaultSecretUriReference(IAzureKeyVaultSecretReference secretOutputReference) { if (!KeyVaultRefs.TryGetValue(secretOutputReference.Resource.Name, out var kv)) { diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 67c092d0413..8bdeb655259 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -365,7 +365,7 @@ public static IResourceBuilder WithAccessKeyAuthenticatio /// The Azure Cosmos DB resource builder. /// The Azure Key Vault resource builder where the connection string used to connect to this AzureCosmosDBResource will be stored. /// A reference to the builder. - public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder) + public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder) { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs index b48179003e3..f21cccc6839 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs @@ -41,7 +41,7 @@ public class AzureCosmosDBResource(string name, Action - internal IKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; } + internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; } private BicepOutputReference NameOutputReference => new("name", this); diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs index 88e441d402f..bd6a3580cb8 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs @@ -13,7 +13,7 @@ namespace Aspire.Hosting.Azure; /// The name of the resource. /// Callback to configure the Azure resources. public class AzureKeyVaultResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IKeyVaultResource + : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IAzureKeyVaultResource { /// /// Gets the "vaultUri" output reference for the Azure Key Vault resource. @@ -31,12 +31,12 @@ public class AzureKeyVaultResource(string name, Action ReferenceExpression.Create($"{VaultUri}"); - BicepOutputReference IKeyVaultResource.VaultUriOutputReference => VaultUri; + BicepOutputReference IAzureKeyVaultResource.VaultUriOutputReference => VaultUri; // In run mode, this is set to the secret client used to access the Azure Key Vault. - internal Func>? SecretResolver { get; set; } + internal Func>? SecretResolver { get; set; } - Func>? IKeyVaultResource.SecretResolver + Func>? IAzureKeyVaultResource.SecretResolver { get => SecretResolver; set => SecretResolver = value; @@ -48,7 +48,7 @@ public class AzureKeyVaultResource(string name, Action /// /// - public IKeyVaultSecretReference GetSecretReference(string secretName) + public IAzureKeyVaultSecretReference GetSecretReference(string secretName) { ArgumentException.ThrowIfNullOrEmpty(secretName, nameof(secretName)); diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs index b5579afd20e..b4cfb718996 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting.Azure; /// /// The name of the secret. /// The Azure Key Vault resource. -internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVaultResource azureKeyVaultResource) : IKeyVaultSecretReference, IValueProvider, IManifestExpressionProvider +internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVaultResource azureKeyVaultResource) : IAzureKeyVaultSecretReference, IValueProvider, IManifestExpressionProvider { /// /// Gets the name of the secret. @@ -20,7 +20,7 @@ internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVa /// /// Gets the Azure Key Vault resource. /// - public IKeyVaultResource Resource => azureKeyVaultResource; + public IAzureKeyVaultResource Resource => azureKeyVaultResource; string IManifestExpressionProvider.ValueExpression => $"{{{azureKeyVaultResource.Name}.secrets.{SecretName}}}"; diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs index 7f147620a2b..60a279045d7 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs @@ -118,7 +118,7 @@ public static IResourceBuilder AsAzurePostgresFlexibleSe /// This requires changes to the application code to use an azure credential to authenticate with the resource. See /// https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-connect-with-managed-identity#connect-using-managed-identity-in-c for more information. /// - /// You can use the method to configure the resource to use password authentication. + /// You can use the method to configure the resource to use password authentication. /// /// /// The following example creates an Azure PostgreSQL Flexible Server resource and referencing that resource in a .NET project. @@ -317,7 +317,7 @@ public static IResourceBuilder WithPassword /// A reference to the builder. public static IResourceBuilder WithPasswordAuthentication( this IResourceBuilder builder, - IResourceBuilder keyVaultBuilder, + IResourceBuilder keyVaultBuilder, IResourceBuilder? userName = null, IResourceBuilder? password = null) { diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs index 34ddbab1d72..0946756806c 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs @@ -33,7 +33,7 @@ public class AzurePostgresFlexibleServerResource(string name, Action - internal IKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; } + internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; } private BicepOutputReference NameOutputReference => new("name", this); diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs index 7c7b844c4e3..ec1cdfee8a9 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs @@ -29,7 +29,7 @@ public class AzureRedisCacheResource(string name, Action - internal IKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; } + internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; } private BicepOutputReference NameOutputReference => new("name", this); diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs index 219fb258009..a0171036463 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs @@ -96,7 +96,7 @@ public static IResourceBuilder AsAzureRedis(this IResourceBuilder /// This requires changes to the application code to use an azure credential to authenticate with the resource. See /// https://github.com/Azure/Microsoft.Azure.StackExchangeRedis for more information. /// - /// You can use the method to configure the resource to use access key authentication. + /// You can use the method to configure the resource to use access key authentication. /// /// /// The following example creates an Azure Cache for Redis resource and referencing that resource in a .NET project. @@ -218,7 +218,7 @@ public static IResourceBuilder WithAccessKeyAuthenticat /// The Azure Cache for Redis resource builder. /// The Azure Key Vault resource builder where the connection string used to connect to this AzureRedisCacheResource will be stored. /// A reference to the builder. - public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder) + public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(keyVaultBuilder); diff --git a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs index c3af1161d3f..a6d47daccd7 100644 --- a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs +++ b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs @@ -362,7 +362,7 @@ private static void ProcessAzureReferences(HashSet azureReferenc return; } - if (value is IKeyVaultSecretReference keyVaultSecretReference) + if (value is IAzureKeyVaultSecretReference keyVaultSecretReference) { azureReferences.Add(keyVaultSecretReference.Resource); return; diff --git a/src/Aspire.Hosting.Azure/IKeyVaultResource.cs b/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs similarity index 80% rename from src/Aspire.Hosting.Azure/IKeyVaultResource.cs rename to src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs index 4760e0e8368..432b072dcf1 100644 --- a/src/Aspire.Hosting.Azure/IKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs @@ -8,7 +8,7 @@ namespace Aspire.Hosting.Azure; /// /// Represents a resource that represents an Azure Key Vault. /// -public interface IKeyVaultResource : IResource, IAzureResource +public interface IAzureKeyVaultResource : IResource, IAzureResource { /// /// Gets the output reference that represents the vault uri for the Azure Key Vault resource. @@ -23,12 +23,12 @@ public interface IKeyVaultResource : IResource, IAzureResource /// /// Gets or sets the secret resolver function used to resolve secrets at runtime. /// - Func>? SecretResolver { get; set; } + Func>? SecretResolver { get; set; } /// /// Gets a secret reference for the specified secret name. /// /// The name of the secret. /// A reference to the secret. - IKeyVaultSecretReference GetSecretReference(string secretName); + IAzureKeyVaultSecretReference GetSecretReference(string secretName); } diff --git a/src/Aspire.Hosting.Azure/IKeyVaultSecretReference.cs b/src/Aspire.Hosting.Azure/IAzureKeyVaultSecretReference.cs similarity index 78% rename from src/Aspire.Hosting.Azure/IKeyVaultSecretReference.cs rename to src/Aspire.Hosting.Azure/IAzureKeyVaultSecretReference.cs index 70f1cb30d58..b0a33b5a536 100644 --- a/src/Aspire.Hosting.Azure/IKeyVaultSecretReference.cs +++ b/src/Aspire.Hosting.Azure/IAzureKeyVaultSecretReference.cs @@ -8,7 +8,7 @@ namespace Aspire.Hosting.Azure; /// /// Represents a reference to a secret in an Azure Key Vault resource. /// -public interface IKeyVaultSecretReference : IValueProvider, IManifestExpressionProvider +public interface IAzureKeyVaultSecretReference : IValueProvider, IManifestExpressionProvider { /// /// Gets the name of the secret. @@ -18,5 +18,5 @@ public interface IKeyVaultSecretReference : IValueProvider, IManifestExpressionP /// /// Gets the Azure Key Vault resource. /// - IKeyVaultResource Resource { get; } -} \ No newline at end of file + IAzureKeyVaultResource Resource { get; } +} diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs index 6e6b8598349..d4a0c90d5cb 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs @@ -278,7 +278,7 @@ await notificationService.PublishUpdateAsync(resource, state => } // Populate secret outputs from key vault (if any) - if (resource is IKeyVaultResource kvr) + if (resource is IAzureKeyVaultResource kvr) { var vaultUri = resource.Outputs[kvr.VaultUriOutputReference.Name] as string ?? throw new InvalidOperationException($"{kvr.VaultUriOutputReference.Name} not found in outputs."); From 056e452268b0e97dc52b82bdf4c0e1efffe2e595 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:40:51 -0700 Subject: [PATCH 05/30] Move database script annotations to their respective projects (#8538) Contributes to #7811 Co-authored-by: Sebastien Ros --- .../PostgresBuilderExtensions.cs | 4 +-- .../PostgresCreateDatabaseScriptAnnotation.cs | 27 +++++++++++++++++++ .../SqlServerBuilderExtensions.cs | 4 +-- ...SqlServerCreateDatabaseScriptAnnotation.cs | 27 +++++++++++++++++++ .../CreationScriptAnnotation.cs | 25 ----------------- 5 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 src/Aspire.Hosting.PostgreSQL/PostgresCreateDatabaseScriptAnnotation.cs create mode 100644 src/Aspire.Hosting.SqlServer/SqlServerCreateDatabaseScriptAnnotation.cs delete mode 100644 src/Aspire.Hosting/ApplicationModel/CreationScriptAnnotation.cs diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs index fc4b463bd24..e2d22a4aedb 100644 --- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs @@ -419,7 +419,7 @@ public static IResourceBuilder WithCreationScript(this ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(script); - builder.WithAnnotation(new CreationScriptAnnotation(script)); + builder.WithAnnotation(new PostgresCreateDatabaseScriptAnnotation(script)); return builder; } @@ -493,7 +493,7 @@ private static string WritePgAdminServerJson(IEnumerable private static async Task CreateDatabaseAsync(NpgsqlConnection npgsqlConnection, PostgresDatabaseResource npgsqlDatabase, IServiceProvider serviceProvider, CancellationToken cancellationToken) { - var scriptAnnotation = npgsqlDatabase.Annotations.OfType().LastOrDefault(); + var scriptAnnotation = npgsqlDatabase.Annotations.OfType().LastOrDefault(); try { diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresCreateDatabaseScriptAnnotation.cs b/src/Aspire.Hosting.PostgreSQL/PostgresCreateDatabaseScriptAnnotation.cs new file mode 100644 index 00000000000..54e64354d71 --- /dev/null +++ b/src/Aspire.Hosting.PostgreSQL/PostgresCreateDatabaseScriptAnnotation.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Postgres; + +/// +/// Represents an annotation for defining a script to create a database in PostgreSQL. +/// +internal sealed class PostgresCreateDatabaseScriptAnnotation : IResourceAnnotation +{ + /// + /// Initializes a new instance of the class. + /// + /// The script used to create the database. + public PostgresCreateDatabaseScriptAnnotation(string script) + { + ArgumentNullException.ThrowIfNull(script); + Script = script; + } + + /// + /// Gets the script used to create the database. + /// + public string Script { get; } +} diff --git a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs index 977ea4c7cd3..a8271f79279 100644 --- a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs @@ -195,7 +195,7 @@ public static IResourceBuilder WithCreationScript(thi ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(script); - builder.WithAnnotation(new CreationScriptAnnotation(script)); + builder.WithAnnotation(new SqlServerCreateDatabaseScriptAnnotation(script)); return builder; } @@ -204,7 +204,7 @@ private static async Task CreateDatabaseAsync(SqlConnection sqlConnection, SqlSe { try { - var scriptAnnotation = sqlDatabase.Annotations.OfType().LastOrDefault(); + var scriptAnnotation = sqlDatabase.Annotations.OfType().LastOrDefault(); if (scriptAnnotation?.Script == null) { diff --git a/src/Aspire.Hosting.SqlServer/SqlServerCreateDatabaseScriptAnnotation.cs b/src/Aspire.Hosting.SqlServer/SqlServerCreateDatabaseScriptAnnotation.cs new file mode 100644 index 00000000000..7591d8fe97c --- /dev/null +++ b/src/Aspire.Hosting.SqlServer/SqlServerCreateDatabaseScriptAnnotation.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Represents an annotation for defining a script to create a database in SQL Server. +/// +internal sealed class SqlServerCreateDatabaseScriptAnnotation : IResourceAnnotation +{ + /// + /// Initializes a new instance of the class. + /// + /// The script used to create the database. + public SqlServerCreateDatabaseScriptAnnotation(string script) + { + ArgumentNullException.ThrowIfNull(script); + Script = script; + } + + /// + /// Gets the script used to create the database. + /// + public string Script { get; } +} diff --git a/src/Aspire.Hosting/ApplicationModel/CreationScriptAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/CreationScriptAnnotation.cs deleted file mode 100644 index f66abcecf83..00000000000 --- a/src/Aspire.Hosting/ApplicationModel/CreationScriptAnnotation.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.ApplicationModel; - -/// -/// Represents an annotation for defining a script to create a resource. -/// -public sealed class CreationScriptAnnotation : IResourceAnnotation -{ - /// - /// Initializes a new instance of the class. - /// - /// The script used to create the resource. - public CreationScriptAnnotation(string script) - { - ArgumentNullException.ThrowIfNull(script); - Script = script; - } - - /// - /// Gets the script used to create the resource. - /// - public string Script { get; } -} From ae94e552205c4ea8f84ae793f3c2629199c1c328 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:47:38 -0700 Subject: [PATCH 06/30] Remove AzureContainerAppEnvironmentResource BicepOutputReferences from public API (#8530) These properties may change over time or be moved in the future. We don't want to expose them just yet. Will expose later if they need to be public. Contributes to #7811 Co-authored-by: Eric Erhardt --- .../AzureContainerAppEnvironmentResource.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs index ce17e9813b3..276cce3e9f6 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs @@ -16,42 +16,42 @@ public class AzureContainerAppEnvironmentResource(string name, Action /// Gets the unique identifier of the Container App Environment. /// - public BicepOutputReference ContainerAppEnvironmentId => new("AZURE_CONTAINER_APPS_ENVIRONMENT_ID", this); + private BicepOutputReference ContainerAppEnvironmentId => new("AZURE_CONTAINER_APPS_ENVIRONMENT_ID", this); /// /// Gets the default domain associated with the Container App Environment. /// - public BicepOutputReference ContainerAppDomain => new("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", this); + private BicepOutputReference ContainerAppDomain => new("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", this); /// /// Gets the URL endpoint of the associated Azure Container Registry. /// - public BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this); + private BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this); /// /// Gets the managed identity ID associated with the Azure Container Registry. /// - public BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this); + private BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this); /// /// Gets the unique identifier of the Log Analytics workspace. /// - public BicepOutputReference LogAnalyticsWorkspaceId => new("AZURE_LOG_ANALYTICS_WORKSPACE_ID", this); + private BicepOutputReference LogAnalyticsWorkspaceId => new("AZURE_LOG_ANALYTICS_WORKSPACE_ID", this); /// /// Gets the principal name of the managed identity. /// - public BicepOutputReference PrincipalName => new("MANAGED_IDENTITY_NAME", this); + private BicepOutputReference PrincipalName => new("MANAGED_IDENTITY_NAME", this); /// /// Gets the principal ID of the managed identity. /// - public BicepOutputReference PrincipalId => new("MANAGED_IDENTITY_PRINCIPAL_ID", this); + private BicepOutputReference PrincipalId => new("MANAGED_IDENTITY_PRINCIPAL_ID", this); /// /// Gets the name of the Container App Environment. /// - public BicepOutputReference ContainerAppEnvironmentName => new("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", this); + private BicepOutputReference ContainerAppEnvironmentName => new("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", this); internal Dictionary VolumeNames { get; } = []; From 211529052dc72c294938a910419596b1e2514c87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:26:26 -0500 Subject: [PATCH 07/30] [release/9.2] Improve path prompt for aspire new command. (#8543) * Improve path prompt for aspire new command. * Turn project option into argument on aspire run. * Fix up message. --------- Co-authored-by: Mitch Denny --- src/Aspire.Cli/Commands/NewCommand.cs | 16 ++++++------ src/Aspire.Cli/Commands/RunCommand.cs | 8 +++--- src/Aspire.Cli/DotNetCliRunner.cs | 2 +- src/Aspire.Cli/Utils/ProjectFileHelper.cs | 32 +++++++++++++++++++++++ 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index f676383e29d..efed8f77e17 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -49,13 +49,13 @@ internal sealed class NewCommand : BaseCommand // interrogate the various options and add them. For now we will // keep it simple. (string TemplateName, string TemplateDescription, string? PathAppendage)[] validTemplates = [ - ("aspire-starter", "Aspire Starter App", "src") , - ("aspire", "Aspire Empty App", "src"), - ("aspire-apphost", "Aspire App Host", null), - ("aspire-servicedefaults", "Aspire Service Defaults", null), - ("aspire-mstest", "Aspire Test Project (MSTest)", null), - ("aspire-nunit", "Aspire Test Project (NUnit)", null), - ("aspire-xunit", "Aspire Test Project (xUnit)", null) + ("aspire-starter", "Aspire Starter App", "./src") , + ("aspire", "Aspire Empty App", "./src"), + ("aspire-apphost", "Aspire App Host", "./"), + ("aspire-servicedefaults", "Aspire Service Defaults", "./"), + ("aspire-mstest", "Aspire Test Project (MSTest)", "./"), + ("aspire-nunit", "Aspire Test Project (NUnit)", "./"), + ("aspire-xunit", "Aspire Test Project (xUnit)", "./") ]; if (parseResult.GetValue("template") is { } templateName && validTemplates.SingleOrDefault(t => t.TemplateName == templateName) is { } template) @@ -92,7 +92,7 @@ private static async Task GetOutputPathAsync(ParseResult parseResult, st { outputPath = await PromptUtils.PromptForStringAsync( "Enter the output path:", - defaultValue: Path.Combine(Environment.CurrentDirectory, pathAppendage ?? string.Empty), + defaultValue: pathAppendage ?? ".", cancellationToken: cancellationToken ); } diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index f5067dffa53..32266daa207 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -23,9 +23,9 @@ public RunCommand(DotNetCliRunner runner) : base("run", "Run an Aspire app host _runner = runner; - var projectOption = new Option("--project"); - projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption); - Options.Add(projectOption); + var projectArgument = new Argument("project"); + projectArgument.Validators.Add(ProjectFileHelper.ValidateProjectArgument); + Arguments.Add(projectArgument); var watchOption = new Option("--watch", "-w"); Options.Add(watchOption); @@ -35,7 +35,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell { using var activity = _activitySource.StartActivity(); - var passedAppHostProjectFile = parseResult.GetValue("--project"); + var passedAppHostProjectFile = parseResult.GetValue("project"); var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); if (effectiveAppHostProjectFile is null) diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index 40eafffa202..0141e8214e3 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -23,7 +23,7 @@ internal sealed class DotNetCliRunner(ILogger logger, IServiceP { using var activity = _activitySource.StartActivity(); - string[] cliArgs = ["msbuild", "-getproperty:IsAspireHost,AspireHostingSDKVersion"]; + string[] cliArgs = ["msbuild", "-getproperty:IsAspireHost,AspireHostingSDKVersion", projectFile.FullName]; string? stdout = null; string? stderr = null; diff --git a/src/Aspire.Cli/Utils/ProjectFileHelper.cs b/src/Aspire.Cli/Utils/ProjectFileHelper.cs index 574c1f23700..d2f0b004728 100644 --- a/src/Aspire.Cli/Utils/ProjectFileHelper.cs +++ b/src/Aspire.Cli/Utils/ProjectFileHelper.cs @@ -47,6 +47,38 @@ internal static class ProjectFileHelper }; } + internal static void ValidateProjectArgument(ArgumentResult result) + { + var value = result.GetValueOrDefault(); + + if (value is null) + { + // Having no value here is fine, but there has to + // be a single csproj file in the current + // working directory. + var csprojFiles = Directory.GetFiles(Environment.CurrentDirectory, "*.csproj"); + + if (csprojFiles.Length > 1) + { + result.AddError("The project argument was not specified and multiple *.csproj files were detected."); + return; + } + else if (csprojFiles.Length == 0) + { + result.AddError("The project argument was not specified and no *.csproj files were detected."); + return; + } + + return; + } + + if (!File.Exists(value.FullName)) + { + result.AddError("The specified project file does not exist."); + return; + } + } + internal static void ValidateProjectOption(OptionResult result) { var value = result.GetValueOrDefault(); From eaa7869f371fba9f8e8e1d3acc1b76aa8d91b0d9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 09:33:33 -0600 Subject: [PATCH 08/30] [release/9.2] Remove null/nullable parameter from DistributedApplicationExecutionContext (#8539) * Make DistributedApplicationExecutionContext.PublisherName not nullable Contributes to #7811 * Improve ctor * Prevent null publisher name --------- Co-authored-by: Sebastien Ros --- src/Aspire.Hosting/DistributedApplicationBuilder.cs | 3 ++- .../DistributedApplicationExecutionContext.cs | 2 +- .../DistributedApplicationExecutionContextOptions.cs | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index dc262198313..9eb7c04ebc4 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -130,7 +130,8 @@ private DistributedApplicationExecutionContextOptions BuildExecutionContextOptio { DistributedApplicationOperation.Run => new DistributedApplicationExecutionContextOptions(operation), DistributedApplicationOperation.Inspect => new DistributedApplicationExecutionContextOptions(operation), - _ => new DistributedApplicationExecutionContextOptions(operation, _innerBuilder.Configuration["Publishing:Publisher"]) + DistributedApplicationOperation.Publish => new DistributedApplicationExecutionContextOptions(operation, _innerBuilder.Configuration["Publishing:Publisher"] ?? "manifest"), + _ => throw new DistributedApplicationException("Invalid operation specified. Valid operations are 'publish', 'inspect', or 'run'.") }; } diff --git a/src/Aspire.Hosting/DistributedApplicationExecutionContext.cs b/src/Aspire.Hosting/DistributedApplicationExecutionContext.cs index 0a362d250ca..119d0ffecdd 100644 --- a/src/Aspire.Hosting/DistributedApplicationExecutionContext.cs +++ b/src/Aspire.Hosting/DistributedApplicationExecutionContext.cs @@ -36,7 +36,7 @@ public DistributedApplicationExecutionContext(DistributedApplicationOperation op /// /// The name of the publisher that is being used if is set to . /// - public string? PublisherName { get; set; } + public string PublisherName { get; set; } private readonly DistributedApplicationExecutionContextOptions? _options; diff --git a/src/Aspire.Hosting/DistributedApplicationExecutionContextOptions.cs b/src/Aspire.Hosting/DistributedApplicationExecutionContextOptions.cs index 95a7b1df8f3..b3965e064b5 100644 --- a/src/Aspire.Hosting/DistributedApplicationExecutionContextOptions.cs +++ b/src/Aspire.Hosting/DistributedApplicationExecutionContextOptions.cs @@ -12,8 +12,9 @@ public class DistributedApplicationExecutionContextOptions /// Constructs a . /// /// Indicates whether the AppHost is running in Publish mode or Run mode. - public DistributedApplicationExecutionContextOptions(DistributedApplicationOperation operation) : this(operation, null) + public DistributedApplicationExecutionContextOptions(DistributedApplicationOperation operation) { + this.Operation = operation; } /// @@ -21,7 +22,7 @@ public DistributedApplicationExecutionContextOptions(DistributedApplicationOpera /// /// Indicates whether the AppHost is running in Publish mode or Run mode. /// The publisher name if in Publish mode. - public DistributedApplicationExecutionContextOptions(DistributedApplicationOperation operation, string? publisherName = null) + public DistributedApplicationExecutionContextOptions(DistributedApplicationOperation operation, string publisherName) { this.Operation = operation; this.PublisherName = publisherName; @@ -38,7 +39,7 @@ public DistributedApplicationExecutionContextOptions(DistributedApplicationOpera public DistributedApplicationOperation Operation { get; } /// - /// The name of the publisher if running in pbublish mode. + /// The name of the publisher if running in publish mode. /// public string? PublisherName { get; } } From ed06d7a307116b1e80ef100004ac5ab5afd2adfc Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Sat, 5 Apr 2025 02:45:15 +1100 Subject: [PATCH 09/30] Adding descriptions to all args/options in CLI. (#8544) (#8552) * Adding descriptions to all args/options in CLI. * Revert project arg to option. * PR feedback. * Rename resource to integration. * Update src/Aspire.Cli/Commands/RunCommand.cs --------- Co-authored-by: David Fowler Co-authored-by: Eric Erhardt --- src/Aspire.Cli/Commands/AddCommand.cs | 16 +++++++++++----- src/Aspire.Cli/Commands/NewCommand.cs | 8 +++++++- src/Aspire.Cli/Commands/PublishCommand.cs | 6 +++++- src/Aspire.Cli/Commands/RootCommand.cs | 5 ++++- src/Aspire.Cli/Commands/RunCommand.cs | 11 +++++++---- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs index 03d9545df9e..be68a9c02b6 100644 --- a/src/Aspire.Cli/Commands/AddCommand.cs +++ b/src/Aspire.Cli/Commands/AddCommand.cs @@ -15,28 +15,34 @@ internal sealed class AddCommand : BaseCommand private readonly DotNetCliRunner _runner; private readonly INuGetPackageCache _nuGetPackageCache; - public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("add", "Add an integration or other resource to the Aspire project.") + public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) + : base("add", "Add an integration to the Aspire project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache)); _runner = runner; _nuGetPackageCache = nuGetPackageCache; - var resourceArgument = new Argument("resource"); - resourceArgument.Arity = ArgumentArity.ZeroOrOne; - Arguments.Add(resourceArgument); + var integrationArgument = new Argument("integration"); + integrationArgument.Description = "The name of the integration to add (e.g. redis, postgres)."; + integrationArgument.Arity = ArgumentArity.ZeroOrOne; + Arguments.Add(integrationArgument); var projectOption = new Option("--project"); + projectOption.Description = "The path to the project file to add the integration to."; projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption); Options.Add(projectOption); var versionOption = new Option("--version", "-v"); + versionOption.Description = "The version of the integration to add."; Options.Add(versionOption); var prereleaseOption = new Option("--prerelease"); + prereleaseOption.Description = "Include pre-release versions of the integration when searching."; Options.Add(prereleaseOption); var sourceOption = new Option("--source", "-s"); + sourceOption.Description = "The NuGet source to use for the integration."; Options.Add(sourceOption); } @@ -46,7 +52,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell try { - var integrationName = parseResult.GetValue("resource"); + var integrationName = parseResult.GetValue("integration"); var passedAppHostProjectFile = parseResult.GetValue("--project"); var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index efed8f77e17..2e095073117 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -15,7 +15,8 @@ internal sealed class NewCommand : BaseCommand private readonly DotNetCliRunner _runner; private readonly INuGetPackageCache _nuGetPackageCache; - public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("new", "Create a new Aspire sample project.") + public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) + : base("new", "Create a new Aspire sample project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache)); @@ -23,19 +24,24 @@ internal sealed class NewCommand : BaseCommand _nuGetPackageCache = nuGetPackageCache; var templateArgument = new Argument("template"); + templateArgument.Description = "The name of the project template to use (e.g. aspire-starter, aspire)."; templateArgument.Arity = ArgumentArity.ZeroOrOne; Arguments.Add(templateArgument); var nameOption = new Option("--name", "-n"); + nameOption.Description = "The name of the project to create."; Options.Add(nameOption); var outputOption = new Option("--output", "-o"); + outputOption.Description = "The output path for the project."; Options.Add(outputOption); var sourceOption = new Option("--source", "-s"); + sourceOption.Description = "The NuGet source to use for the project templates."; Options.Add(sourceOption); var templateVersionOption = new Option("--version", "-v"); + templateVersionOption.Description = "The version of the project templates to use."; Options.Add(templateVersionOption); } diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index f25893beb53..df9c19d8d97 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -15,19 +15,23 @@ internal sealed class PublishCommand : BaseCommand private readonly ActivitySource _activitySource = new ActivitySource(nameof(PublishCommand)); private readonly DotNetCliRunner _runner; - public PublishCommand(DotNetCliRunner runner) : base("publish", "Generates deployment artifacts for an Aspire app host project.") + public PublishCommand(DotNetCliRunner runner) + : base("publish", "Generates deployment artifacts for an Aspire app host project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); _runner = runner; var projectOption = new Option("--project"); + projectOption.Description = "The path to the Aspire app host project file."; projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption); Options.Add(projectOption); var publisherOption = new Option("--publisher", "-p"); + publisherOption.Description = "The name of the publisher to use."; Options.Add(publisherOption); var outputPath = new Option("--output-path", "-o"); + outputPath.Description = "The output path for the generated artifacts."; outputPath.DefaultValueFactory = (result) => Path.Combine(Environment.CurrentDirectory); Options.Add(outputPath); } diff --git a/src/Aspire.Cli/Commands/RootCommand.cs b/src/Aspire.Cli/Commands/RootCommand.cs index a2f7cd1db05..22d4ad6bbdf 100644 --- a/src/Aspire.Cli/Commands/RootCommand.cs +++ b/src/Aspire.Cli/Commands/RootCommand.cs @@ -14,13 +14,16 @@ namespace Aspire.Cli.Commands; internal sealed class RootCommand : BaseRootCommand { - public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addCommand, PublishCommand publishCommand) : base("Aspire CLI") + public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addCommand, PublishCommand publishCommand) + : base("The Aspire CLI can be used to create, run, and publish Aspire-based applications.") { var debugOption = new Option("--debug", "-d"); + debugOption.Description = "Enable debug logging to the console."; debugOption.Recursive = true; Options.Add(debugOption); var waitForDebuggerOption = new Option("--wait-for-debugger", "-w"); + waitForDebuggerOption.Description = "Wait for a debugger to attach before executing the command."; waitForDebuggerOption.Recursive = true; waitForDebuggerOption.DefaultValueFactory = (result) => false; diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 32266daa207..69e4ffcb1f4 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -17,17 +17,20 @@ internal sealed class RunCommand : BaseCommand private readonly ActivitySource _activitySource = new ActivitySource(nameof(RunCommand)); private readonly DotNetCliRunner _runner; - public RunCommand(DotNetCliRunner runner) : base("run", "Run an Aspire app host in development mode.") + public RunCommand(DotNetCliRunner runner) + : base("run", "Run an Aspire app host in development mode.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); _runner = runner; - var projectArgument = new Argument("project"); - projectArgument.Validators.Add(ProjectFileHelper.ValidateProjectArgument); - Arguments.Add(projectArgument); + var projectOption = new Option("--project"); + projectOption.Description = "The path to the Aspire app host project file."; + projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption); + Options.Add(projectOption); var watchOption = new Option("--watch", "-w"); + watchOption.Description = "Start project resources in watch mode."; Options.Add(watchOption); } From e6fb0df38a700ecdaa9b1e0bc358b93a792bd16a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:05:38 -0700 Subject: [PATCH 10/30] [release/9.2] Switch to null as default for owner and group in WithContainerFiles (#8558) * Switch to null as default for owner and group in WithContainerFiles * Fix failing test * Update doc comment * Call out that 0 is root --------- Co-authored-by: David Negstad --- .../ContainerFileSystemCallbackAnnotation.cs | 8 ++++---- .../ContainerResourceBuilderExtensions.cs | 12 ++++++------ src/Aspire.Hosting/Dcp/Model/Container.cs | 6 ++++-- .../AddPostgresTests.cs | 8 ++++---- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ContainerFileSystemCallbackAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ContainerFileSystemCallbackAnnotation.cs index 736dd24f297..ed8ce8f63ef 100644 --- a/src/Aspire.Hosting/ApplicationModel/ContainerFileSystemCallbackAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ContainerFileSystemCallbackAnnotation.cs @@ -82,14 +82,14 @@ public sealed class ContainerFileSystemCallbackAnnotation : IResourceAnnotation public required string DestinationPath { get; init; } /// - /// The UID of the default owner for files/directories to be created or updated in the container. Defaults to 0 for root. + /// The UID of the default owner for files/directories to be created or updated in the container. The UID defaults to 0 for root if null. /// - public int DefaultOwner { get; init; } + public int? DefaultOwner { get; init; } /// - /// The GID of the default group for files/directories to be created or updated in the container. Defaults to 0 for root. + /// The GID of the default group for files/directories to be created or updated in the container. The GID defaults to 0 for root if null. /// - public int DefaultGroup { get; init; } + public int? DefaultGroup { get; init; } /// /// The umask to apply to files or folders without an explicit mode permission. If set to null, a default umask value of 0022 (octal) will be used. diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index bfa71505fd3..3b1d47dc1e1 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -706,8 +706,8 @@ public static IResourceBuilder WithBuildSecret(this IResourceBuilder bu /// The resource builder for the container resource. /// The destination (absolute) path in the container. /// The file system entries to create. - /// The default owner UID for the created or updated file system. Defaults to 0 for root. - /// The default group ID for the created or updated file system. Defaults to 0 for root. + /// The default owner UID for the created or updated file system. Defaults to 0 for root if not set. + /// The default group ID for the created or updated file system. Defaults to 0 for root if not set. /// The umask permissions to exclude from the default file and folder permissions. This takes away (rather than granting) default permissions to files and folders without an explicit mode permission set. /// The . /// @@ -741,7 +741,7 @@ public static IResourceBuilder WithBuildSecret(this IResourceBuilder bu /// defaultOwner: 1000); /// /// - public static IResourceBuilder WithContainerFiles(this IResourceBuilder builder, string destinationPath, IEnumerable entries, int defaultOwner = 0, int defaultGroup = 0, UnixFileMode? umask = null) where T : ContainerResource + public static IResourceBuilder WithContainerFiles(this IResourceBuilder builder, string destinationPath, IEnumerable entries, int? defaultOwner = null, int? defaultGroup = null, UnixFileMode? umask = null) where T : ContainerResource { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(destinationPath); @@ -769,8 +769,8 @@ public static IResourceBuilder WithContainerFiles(this IResourceBuilder /// The resource builder for the container resource. /// The destination (absolute) path in the container. /// The callback that will be invoked when the resource is being created. - /// The default owner UID for the created or updated file system. Defaults to 0 for root. - /// The default group ID for the created or updated file system. Defaults to 0 for root. + /// The default owner UID for the created or updated file system. Defaults to 0 for root if not set. + /// The default group ID for the created or updated file system. Defaults to 0 for root if not set. /// The umask permissions to exclude from the default file and folder permissions. This takes away (rather than granting) default permissions to files and folders without an explicit mode permission set. /// The . /// @@ -814,7 +814,7 @@ public static IResourceBuilder WithContainerFiles(this IResourceBuilder /// }); /// /// - public static IResourceBuilder WithContainerFiles(this IResourceBuilder builder, string destinationPath, Func>> callback, int defaultOwner = 0, int defaultGroup = 0, UnixFileMode? umask = null) where T : ContainerResource + public static IResourceBuilder WithContainerFiles(this IResourceBuilder builder, string destinationPath, Func>> callback, int? defaultOwner = null, int? defaultGroup = null, UnixFileMode? umask = null) where T : ContainerResource { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(destinationPath); diff --git a/src/Aspire.Hosting/Dcp/Model/Container.cs b/src/Aspire.Hosting/Dcp/Model/Container.cs index 4a1af7312c5..0e1ca6bf268 100644 --- a/src/Aspire.Hosting/Dcp/Model/Container.cs +++ b/src/Aspire.Hosting/Dcp/Model/Container.cs @@ -295,11 +295,13 @@ internal sealed class ContainerCreateFileSystem : IEquatable Date: Fri, 4 Apr 2025 12:41:17 -0700 Subject: [PATCH 11/30] Bump the version of MEAI, Remove lift of dependencies, Fix dependency issue (#8560) --- eng/Versions.props | 2 +- .../OpenAIEndToEnd.WebStory.csproj | 2 -- .../Aspire.Azure.AI.OpenAI.csproj | 2 -- .../Aspire.Microsoft.Data.SqlClient.csproj | 1 - src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj | 2 -- .../Aspire.OpenAI/MEAIPackageOverrides.targets | 13 ------------- .../Aspire.Azure.AI.OpenAI.Tests.csproj | 2 -- .../Aspire.Microsoft.Data.SqlClient.Tests.csproj | 1 + .../Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj | 2 -- 9 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 src/Components/Aspire.OpenAI/MEAIPackageOverrides.targets diff --git a/eng/Versions.props b/eng/Versions.props index 9fb2dae920b..820ab4b4ef2 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -37,7 +37,7 @@ 9.0.0-beta.25164.2 9.0.0-beta.25164.2 - 9.4.0-preview.1.25201.1 + 9.4.0-preview.1.25202.3 9.3.0 9.3.0 diff --git a/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj b/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj index e9ab77317ff..ede52a85213 100644 --- a/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj +++ b/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj @@ -11,6 +11,4 @@ - - diff --git a/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj b/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj index eb6984c1f0d..45173a72455 100644 --- a/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj +++ b/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj @@ -30,6 +30,4 @@ - - diff --git a/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj b/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj index e3e404d2eaf..e7740e1af77 100644 --- a/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj +++ b/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj @@ -22,7 +22,6 @@ - diff --git a/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj b/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj index c7ce004d418..80805ec79d3 100644 --- a/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj +++ b/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj @@ -21,6 +21,4 @@ - - diff --git a/src/Components/Aspire.OpenAI/MEAIPackageOverrides.targets b/src/Components/Aspire.OpenAI/MEAIPackageOverrides.targets deleted file mode 100644 index 038ac594e4c..00000000000 --- a/src/Components/Aspire.OpenAI/MEAIPackageOverrides.targets +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj b/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj index 2d919f20164..e772334625f 100644 --- a/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj +++ b/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj @@ -17,6 +17,4 @@ - - diff --git a/tests/Aspire.Microsoft.Data.SqlClient.Tests/Aspire.Microsoft.Data.SqlClient.Tests.csproj b/tests/Aspire.Microsoft.Data.SqlClient.Tests/Aspire.Microsoft.Data.SqlClient.Tests.csproj index b3c44e2d25c..e384c9a3f30 100644 --- a/tests/Aspire.Microsoft.Data.SqlClient.Tests/Aspire.Microsoft.Data.SqlClient.Tests.csproj +++ b/tests/Aspire.Microsoft.Data.SqlClient.Tests/Aspire.Microsoft.Data.SqlClient.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj b/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj index 90014430aa3..aa1e5e7f7c4 100644 --- a/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj +++ b/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj @@ -17,6 +17,4 @@ - - From 56b84beefded7df81cd8fd6a57cc424d6385bf55 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 21:50:39 -0700 Subject: [PATCH 12/30] [release/9.2] CLI publishing failure fixes. (#8564) * Make publishers prompt async for faster cancellation. * Make AppHost crash if no backchannel is attached and publishing fails. * Switch LogError to LogDebug (whoops) * Return correct result from TryGetContainerImageName in compose publisher. Add Dockerfile resource to playground for verification. * Improves publisher failure UX. * Update src/Aspire.Cli/Commands/PublishCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Mitch Denny Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../publishers/Publishers.AppHost/Program.cs | 2 + .../Publishers.AppHost/qots/Dockerfile | 12 +++++ .../Publishers.AppHost/qots/qots.go | 28 ++++++++++ src/Aspire.Cli/Commands/PublishCommand.cs | 51 +++++++++++++++---- .../DockerComposePublishingContext.cs | 2 +- .../DistributedApplicationRunner.cs | 5 ++ .../ResourceContainerImageBuilder.cs | 2 +- 7 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 playground/publishers/Publishers.AppHost/qots/Dockerfile create mode 100644 playground/publishers/Publishers.AppHost/qots/qots.go diff --git a/playground/publishers/Publishers.AppHost/Program.cs b/playground/publishers/Publishers.AppHost/Program.cs index 087b2a3e126..0aa9aa1a7c0 100644 --- a/playground/publishers/Publishers.AppHost/Program.cs +++ b/playground/publishers/Publishers.AppHost/Program.cs @@ -47,6 +47,8 @@ .WithEnvironment("P3", param3) .WithReference(backend).WaitFor(backend); +builder.AddDockerfile("mycontainer", "qots"); + #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code diff --git a/playground/publishers/Publishers.AppHost/qots/Dockerfile b/playground/publishers/Publishers.AppHost/qots/Dockerfile new file mode 100644 index 00000000000..571b7aa9e44 --- /dev/null +++ b/playground/publishers/Publishers.AppHost/qots/Dockerfile @@ -0,0 +1,12 @@ +# Stage 1: Build the Go program +ARG GO_VERSION=1.23 +FROM mcr.microsoft.com/oss/go/microsoft/golang:${GO_VERSION} AS builder +WORKDIR /app +COPY . . +RUN go build qots.go + +# Stage 2: Run the Go program +FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 +WORKDIR /app +COPY --from=builder /app/qots . +CMD ["./qots"] diff --git a/playground/publishers/Publishers.AppHost/qots/qots.go b/playground/publishers/Publishers.AppHost/qots/qots.go new file mode 100644 index 00000000000..8e990937811 --- /dev/null +++ b/playground/publishers/Publishers.AppHost/qots/qots.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "math/rand" + "runtime" + "time" +) + +func main() { + fmt.Println("Go runtime version:", runtime.Version()) + + quotes := []string{ + "With great power comes great responsibility. - Spider-Man", + "I'm Batman. - Batman", + "I am Iron Man. - Iron Man", + "Why so serious? - The Joker", + "I'm always angry. - The Hulk", + } + + rand.Seed(time.Now().UnixNano()) + + for { + quote := quotes[rand.Intn(len(quotes))] + fmt.Println(quote) + time.Sleep(time.Second) + } +} diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index df9c19d8d97..6e03627cdf2 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -125,7 +125,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell .HighlightStyle(Style.Parse("darkmagenta")) .AddChoices(publishers!); - publisher = AnsiConsole.Prompt(publisherPrompt); + publisher = await AnsiConsole.PromptAsync(publisherPrompt, cancellationToken); } AnsiConsole.MarkupLine($":hammer_and_wrench: Generating artifacts for '{publisher}' publisher..."); @@ -144,7 +144,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var backchannelCompletionSource = new TaskCompletionSource(); - var launchingAppHostTask = context.AddTask("Launching apphost"); + var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); launchingAppHostTask.IsIndeterminate(); launchingAppHostTask.StartTask(); @@ -159,6 +159,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + launchingAppHostTask.Description = $":check_mark: Launching apphost"; launchingAppHostTask.Value = 100; launchingAppHostTask.StopTask(); @@ -176,37 +177,67 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell progressTasks.Add(publishingActivity.Id, progressTask); } - progressTask.Description = $"{publishingActivity.StatusText}"; + progressTask.Description = $":play_button: {publishingActivity.StatusText}"; - if (publishingActivity.IsComplete) + if (publishingActivity.IsComplete && !publishingActivity.IsError) { + progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; progressTask.Value = 100; progressTask.StopTask(); } else if (publishingActivity.IsError) { - progressTask.Value = 100; - progressTask.StopTask(); + progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; + progressTask.Value = 0; + break; + } + else + { + // Keep going man! } } + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + // When we are running in publish mode we don't want the app host to // stop itself while we might still be streaming data back across // the RPC backchannel. So we need to take responsibility for stopping // the app host. If the CLI exits/crashes without explicitly stopping // the app host the orphan detector in the app host will kick in. - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - return await pendingRun; + if (progressTasks.Any(kvp => !kvp.Value.IsFinished)) + { + // Depending on the failure the publisher may return a zero + // exit code. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + + // If we are in the state where we've detected an error because there + // is an incomplete task then we stop the app host, but depending on + // where/how the failure occured, we might still get a zero exit + // code. If we get a non-zero exit code we want to return that + // as it might be useful for diagnostic purposes, however if we don't + // get a non-zero exit code we want to return our built-in exit code + // for failed artifact build. + return exitCode == 0 ? ExitCodeConstants.FailedToBuildArtifacts : exitCode; + } + else + { + // If we are here then all the tasks are finished and we can + // stop the app host. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + return exitCode; // should be zero for orderly shutdown but we pass it along anyway. + } }); if (exitCode != 0) { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The build failed with exit code {exitCode}. For more information run with --debug switch.[/]"); + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Publishing artifacts failed with exit code {exitCode}. For more information run with --debug switch.[/]"); return ExitCodeConstants.FailedToBuildArtifacts; } else { - AnsiConsole.MarkupLine($"[green bold]:thumbs_up: The build completed successfully to: {fullyQualifiedOutputPath}[/]"); + AnsiConsole.MarkupLine($"[green bold]:thumbs_up: Successfully published artifacts to: {fullyQualifiedOutputPath}[/]"); return ExitCodeConstants.Success; } } diff --git a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs index 0de4357992e..34b303291d0 100644 --- a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs +++ b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs @@ -261,7 +261,7 @@ private bool TryGetContainerImageName(IResource resourceInstance, out string? co $"{resourceInstance.Name}:latest"); containerImageName = $"${{{imageEnvName}}}"; - return false; + return true; } return resourceInstance.TryGetContainerImageName(out containerImageName); diff --git a/src/Aspire.Hosting/DistributedApplicationRunner.cs b/src/Aspire.Hosting/DistributedApplicationRunner.cs index d953dfd561e..8af3ce11a76 100644 --- a/src/Aspire.Hosting/DistributedApplicationRunner.cs +++ b/src/Aspire.Hosting/DistributedApplicationRunner.cs @@ -67,6 +67,11 @@ await eventing.PublishAsync( logger.LogError(ex, "Failed to publish the distributed application."); publishingActivity.IsError = true; await activityReporter.UpdateActivityAsync(publishingActivity, stoppingToken).ConfigureAwait(false); + + if (!backchannelService.IsBackchannelExpected) + { + lifetime.StopApplication(); + } } } } diff --git a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs index 665b3e91847..cd463079792 100644 --- a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs +++ b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs @@ -140,7 +140,7 @@ private async Task BuildProjectContainerImageAsync(IResource resource, C publishingActivity.IsComplete = true; await activityReporter.UpdateActivityAsync(publishingActivity, cancellationToken).ConfigureAwait(false); - logger.LogError( + logger.LogDebug( ".NET CLI completed with exit code: {ExitCode}", process.ExitCode); From c444919cb9000530841b00020d334982ea4de8b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 21:51:02 -0700 Subject: [PATCH 13/30] [CI] Use an in-repo copy of devcerts installation script (#8565) Co-authored-by: Ankit Jain --- eng/pipelines/templates/BuildAndTest.yml | 7 +- tests/external-scripts/README.md | 3 + tests/external-scripts/common.sh | 98 +++++++++++++++++++ .../ubuntu-create-dotnet-devcert.sh | 8 ++ tests/helix/send-to-helix-inner.proj | 11 +-- 5 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 tests/external-scripts/README.md create mode 100644 tests/external-scripts/common.sh create mode 100755 tests/external-scripts/ubuntu-create-dotnet-devcert.sh diff --git a/eng/pipelines/templates/BuildAndTest.yml b/eng/pipelines/templates/BuildAndTest.yml index 174fe6fd905..fc2b4ee4f8a 100644 --- a/eng/pipelines/templates/BuildAndTest.yml +++ b/eng/pipelines/templates/BuildAndTest.yml @@ -60,12 +60,7 @@ steps: - ${{ if and(eq(parameters.runAsPublic, 'true'), eq(parameters.runPipelineTests, 'true')) }}: # non-helix tests - ${{ if ne(parameters.isWindows, 'true') }}: - - script: mkdir ${{ parameters.repoArtifactsPath }}/devcert-scripts && - cd ${{ parameters.repoArtifactsPath }}/devcert-scripts && - wget https://raw.githubusercontent.com/BorisWilhelms/create-dotnet-devcert/main/scripts/ubuntu-create-dotnet-devcert.sh && - wget https://raw.githubusercontent.com/BorisWilhelms/create-dotnet-devcert/main/scripts/common.sh && - chmod +x ubuntu-create-dotnet-devcert.sh && - ./ubuntu-create-dotnet-devcert.sh + - script: $(Build.SourcesDirectory)/tests/external-scripts/ubuntu-create-dotnet-devcert.sh displayName: Install devcerts - ${{ if eq(parameters.isWindows, 'true') }}: diff --git a/tests/external-scripts/README.md b/tests/external-scripts/README.md new file mode 100644 index 00000000000..5099100c013 --- /dev/null +++ b/tests/external-scripts/README.md @@ -0,0 +1,3 @@ +# external-scripts + +This is a copy of scripts from https://github.com/BorisWilhelms/create-dotnet-devcert/ . This is being used for now as `dotnet dev-certs https --trust` doesn't seem to be working on our CI. diff --git a/tests/external-scripts/common.sh b/tests/external-scripts/common.sh new file mode 100644 index 00000000000..3dcd708482c --- /dev/null +++ b/tests/external-scripts/common.sh @@ -0,0 +1,98 @@ +#!/bin/sh +SAVE=0 + +usage() { + echo "Usage: $0 [-s]" + echo "Generates a valid ASP.NET Core self-signed certificate for the local machine." + echo "The certificate will be imported into the system's certificate store and into various other places." + echo " -s: Also saves the generated crtfile to the home directory" + exit 1 +} + +while getopts "sh" opt +do + case "$opt" in + s) + SAVE=1 + ;; + h) + usage + exit 1 + ;; + *) + ;; + esac +done + +TMP_PATH=/var/tmp/localhost-dev-cert +if [ ! -d $TMP_PATH ]; then + mkdir $TMP_PATH +fi + +cleanup() { + rm -R $TMP_PATH +} + +KEYFILE=$TMP_PATH/dotnet-devcert.key +CRTFILE=$TMP_PATH/dotnet-devcert.crt +PFXFILE=$TMP_PATH/dotnet-devcert.pfx + +NSSDB_PATHS="$HOME/.pki/nssdb \ + $HOME/snap/chromium/current/.pki/nssdb \ + $HOME/snap/postman/current/.pki/nssdb" + +CONF_PATH=$TMP_PATH/localhost.conf +cat >> $CONF_PATH <$(RepoRoot).config/dotnet-tools.json <_DotNetToolJsonContent>$([System.IO.File]::ReadAllText($(_DotNetToolJsonPath))) - <_CreateDotNetDevCertsDirectory>$(ArtifactsObjDir)create-dotnet-devcert - <_AzureFunctionsCliUrl Condition="'$(OS)' == 'Windows_NT'">https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.6280/Azure.Functions.Cli.min.win-x64_net8.4.0.6280.zip <_AzureFunctionsCliUrl Condition="'$(OS)' != 'Windows_NT'">https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.6280/Azure.Functions.Cli.linux-x64_net8.4.0.6280.zip <_DefaultSdkDirNameForTests>dotnet-tests - - _StageCreateDotNetDevCertScripts @@ -164,11 +160,6 @@ - - - - - @@ -187,7 +178,7 @@ - + From 0d7636c655e0573a7ee053d22b3a73361cce6afc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 5 Apr 2025 08:54:34 -0700 Subject: [PATCH 14/30] Split out build step in publisher. (#8568) Co-authored-by: Mitch Denny --- src/Aspire.Cli/Commands/PublishCommand.cs | 10 +++++- src/Aspire.Cli/Commands/RunCommand.cs | 13 +++---- src/Aspire.Cli/DotNetCliRunner.cs | 43 +++++++++++++++++++++++ src/Aspire.Cli/Utils/AppHostHelper.cs | 10 ++++++ 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 6e03627cdf2..45bf36f6660 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -62,6 +62,14 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell return ExitCodeConstants.FailedToDotnetRunAppHost; } + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + + if (buildExitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } + var publisher = parseResult.GetValue("--publisher"); var outputPath = parseResult.GetValue("--output-path"); var fullyQualifiedOutputPath = Path.GetFullPath(outputPath ?? "."); @@ -81,7 +89,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var pendingInspectRun = _runner.RunAsync( effectiveAppHostProjectFile, false, - false, + true, ["--operation", "inspect"], null, backchannelCompletionSource, diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 69e4ffcb1f4..955a2dd8215 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -80,12 +80,13 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var watch = parseResult.GetValue("--watch"); - await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":hammer_and_wrench: Building app host...", async context => { - await _runner.BuildAsync(effectiveAppHostProjectFile, cancellationToken).ConfigureAwait(false); - }); + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + + if (buildExitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } var backchannelCompletitionSource = new TaskCompletionSource(); diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index 0141e8214e3..7833fedf452 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -77,6 +77,49 @@ internal sealed class DotNetCliRunner(ILogger logger, IServiceP } } + public async Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, CancellationToken cancellationToken) + { + using var activity = _activitySource.StartActivity(); + + string[] cliArgs = [ + "msbuild", + $"-getProperty:{string.Join(",", properties)}", + $"-getItem:{string.Join(",", items)}", + projectFile.FullName + ]; + + string? stdout = null; + string? stderr = null; + + var exitCode = await ExecuteAsync( + cliArgs, + null, + projectFile.Directory!, + null, + (_, output, error) => { + stdout = output.ReadToEnd(); + stderr = error.ReadToEnd(); + }, + cancellationToken); + + if (exitCode != 0) + { + logger.LogError( + "Failed to get items and properties from project. Exit code was: {ExitCode}. See debug logs for more details. Stderr: {Stderr}, Stdout: {Stdout}", + exitCode, + stderr, + stdout + ); + + return (exitCode, null); + } + else + { + var json = JsonDocument.Parse(stdout!); + return (exitCode, json); + } + } + public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, CancellationToken cancellationToken) { using var activity = _activitySource.StartActivity(); diff --git a/src/Aspire.Cli/Utils/AppHostHelper.cs b/src/Aspire.Cli/Utils/AppHostHelper.cs index 6f9e068fec1..c6824f09609 100644 --- a/src/Aspire.Cli/Utils/AppHostHelper.cs +++ b/src/Aspire.Cli/Utils/AppHostHelper.cs @@ -62,4 +62,14 @@ internal static class AppHostHelper return appHostInformationResult; } + + internal static async Task BuildAppHostAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + { + return await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync(":hammer_and_wrench: Building app host...", async context => { + return await runner.BuildAsync(projectFile, cancellationToken).ConfigureAwait(false); + }); + } } \ No newline at end of file From 1fa069a037c8c8b6fc07f3eb9c80d4109f9fb559 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 8 Apr 2025 02:28:46 +1000 Subject: [PATCH 15/30] Fix up project switch. (#8584) --- src/Aspire.Cli/Commands/RunCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 955a2dd8215..b7d9b1e70c1 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -38,7 +38,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell { using var activity = _activitySource.StartActivity(); - var passedAppHostProjectFile = parseResult.GetValue("project"); + var passedAppHostProjectFile = parseResult.GetValue("--project"); var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); if (effectiveAppHostProjectFile is null) From 9a04f94952dcc8a7cffaaa948a3e47877dec8eb7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 10:28:55 -0600 Subject: [PATCH 16/30] [release/9.2] PublishingActivityProgressReporter fixes (#8578) * PublishingActivityReporter fixes * Introduce lock around status updates. * Lock on status updates. * Clean out unnecessary files from PR. * Extra file to remove. * Remove debugging code. --------- Co-authored-by: Mitch Denny --- .../Publishers.AppHost/docker-compose.yaml | 15 ++++ src/Aspire.Cli/Commands/PublishCommand.cs | 12 ++- .../Backchannel/AppHostRpcTarget.cs | 14 +-- .../DistributedApplicationRunner.cs | 12 ++- .../PublishingActivityProgressReporter.cs | 88 +++++++++++++++---- .../ResourceContainerImageBuilder.cs | 20 +++-- 6 files changed, 117 insertions(+), 44 deletions(-) diff --git a/playground/publishers/Publishers.AppHost/docker-compose.yaml b/playground/publishers/Publishers.AppHost/docker-compose.yaml index d069b28070a..468e2f06cc2 100644 --- a/playground/publishers/Publishers.AppHost/docker-compose.yaml +++ b/playground/publishers/Publishers.AppHost/docker-compose.yaml @@ -22,6 +22,9 @@ services: ports: - "8002:8001" - "8004:8003" + depends_on: + pg: + condition: "service_started" networks: - "aspire" api: @@ -37,6 +40,11 @@ services: ports: - "8006:8005" - "8008:8007" + depends_on: + pg: + condition: "service_started" + dbsetup: + condition: "service_completed_successfully" networks: - "aspire" sqlserver: @@ -70,6 +78,13 @@ services: ports: - "8011:8010" - "8013:8012" + depends_on: + api: + condition: "service_started" + networks: + - "aspire" + mycontainer: + image: "${MYCONTAINER_IMAGE}" networks: - "aspire" networks: diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 45bf36f6660..cce35f56d32 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -152,7 +152,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var backchannelCompletionSource = new TaskCompletionSource(); - var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); + var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); launchingAppHostTask.IsIndeterminate(); launchingAppHostTask.StartTask(); @@ -167,7 +167,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); - launchingAppHostTask.Description = $":check_mark: Launching apphost"; + launchingAppHostTask.Description = $":check_mark: Launching apphost"; launchingAppHostTask.Value = 100; launchingAppHostTask.StopTask(); @@ -185,17 +185,17 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell progressTasks.Add(publishingActivity.Id, progressTask); } - progressTask.Description = $":play_button: {publishingActivity.StatusText}"; + progressTask.Description = $":play_button: {publishingActivity.StatusText}"; if (publishingActivity.IsComplete && !publishingActivity.IsError) { - progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; + progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; progressTask.Value = 100; progressTask.StopTask(); } else if (publishingActivity.IsError) { - progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; + progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; progressTask.Value = 0; break; } @@ -205,8 +205,6 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell } } - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - // When we are running in publish mode we don't want the app host to // stop itself while we might still be streaming data back across // the RPC backchannel. So we need to take responsibility for stopping diff --git a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs index 328d4ff457d..667742b7210 100644 --- a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs +++ b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs @@ -28,9 +28,9 @@ IHostApplicationLifetime lifetime { while (cancellationToken.IsCancellationRequested == false) { - var publishingActivity = await activityReporter.ActivitiyUpdated.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + var publishingActivityStatus = await activityReporter.ActivityStatusUpdated.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - if (publishingActivity == null) + if (publishingActivityStatus == null) { // If the publishing activity is null, it means that the activity has been removed. // This can happen if the activity is complete or an error occurred. @@ -38,13 +38,13 @@ IHostApplicationLifetime lifetime } yield return ( - publishingActivity.Id, - publishingActivity.StatusMessage, - publishingActivity.IsComplete, - publishingActivity.IsError + publishingActivityStatus.Activity.Id, + publishingActivityStatus.StatusText, + publishingActivityStatus.IsComplete, + publishingActivityStatus.IsError ); - if ( publishingActivity.IsPrimary &&(publishingActivity.IsComplete || publishingActivity.IsError)) + if ( publishingActivityStatus.Activity.IsPrimary &&(publishingActivityStatus.IsComplete || publishingActivityStatus.IsError)) { // If the activity is complete or an error and it is the primary activity, // we can stop listening for updates. diff --git a/src/Aspire.Hosting/DistributedApplicationRunner.cs b/src/Aspire.Hosting/DistributedApplicationRunner.cs index 8af3ce11a76..30285a6e2a6 100644 --- a/src/Aspire.Hosting/DistributedApplicationRunner.cs +++ b/src/Aspire.Hosting/DistributedApplicationRunner.cs @@ -49,8 +49,10 @@ await eventing.PublishAsync( new AfterPublishEvent(serviceProvider, model), stoppingToken ).ConfigureAwait(false); - publishingActivity.IsComplete = true; - await activityReporter.UpdateActivityAsync(publishingActivity, stoppingToken).ConfigureAwait(false); + await activityReporter.UpdateActivityStatusAsync( + publishingActivity, + (status) => status with { IsComplete = true }, + stoppingToken).ConfigureAwait(false); // If we are running in publish mode and a backchannel is being // used then we don't want to stop the app host. Instead the @@ -65,8 +67,10 @@ await eventing.PublishAsync( catch (Exception ex) { logger.LogError(ex, "Failed to publish the distributed application."); - publishingActivity.IsError = true; - await activityReporter.UpdateActivityAsync(publishingActivity, stoppingToken).ConfigureAwait(false); + await activityReporter.UpdateActivityStatusAsync( + publishingActivity, + (status) => status with { IsError = true }, + stoppingToken).ConfigureAwait(false); if (!backchannelService.IsBackchannelExpected) { diff --git a/src/Aspire.Hosting/Publishing/PublishingActivityProgressReporter.cs b/src/Aspire.Hosting/Publishing/PublishingActivityProgressReporter.cs index bea1024aa3e..b3592b7b6fa 100644 --- a/src/Aspire.Hosting/Publishing/PublishingActivityProgressReporter.cs +++ b/src/Aspire.Hosting/Publishing/PublishingActivityProgressReporter.cs @@ -14,10 +14,9 @@ namespace Aspire.Hosting.Publishing; [Experimental("ASPIREPUBLISHERS001")] public sealed class PublishingActivity { - internal PublishingActivity(string id, string initialStatusText, bool isPrimary = false) + internal PublishingActivity(string id, bool isPrimary = false) { Id = id; - StatusMessage = initialStatusText; IsPrimary = isPrimary; } @@ -27,25 +26,41 @@ internal PublishingActivity(string id, string initialStatusText, bool isPrimary public string Id { get; private set; } /// - /// Status message of the publishing activity. + /// Indicates whether the publishing activity is the primary activity. /// - public string StatusMessage { get; set; } + public bool IsPrimary { get; private set; } /// - /// Indicates whether the publishing activity is complete. + /// The status text of the publishing activity. /// - public bool IsComplete { get; set; } + public PublishingActivityStatus? LastStatus { get; internal set; } +} +/// +/// Represents the status of a publishing activity. +/// +[Experimental("ASPIREPUBLISHERS001")] +public sealed record PublishingActivityStatus +{ /// - /// Indicates whether the publishing activity is the primary activity. + /// The publishing activity associated with this status. /// - public bool IsPrimary { get; private set; } + public required PublishingActivity Activity { get; init; } /// - /// Indicates whether the publishing activity has encountered an error. + /// The status text of the publishing activity. /// - public bool IsError { get; set; } + public required string StatusText { get; init; } + /// + /// Indicates whether the publishing activity is complete. + /// + public required bool IsComplete { get; init; } + + /// + /// Indicates whether the publishing activity encountered an error. + /// + public required bool IsError { get; init; } } /// @@ -73,31 +88,68 @@ public interface IPublishingActivityProgressReporter /// Updates the status of an existing publishing activity. /// /// The activity with updated properties. + /// /// The cancellation token. /// - Task UpdateActivityAsync(PublishingActivity publishingActivity, CancellationToken cancellationToken); + Task UpdateActivityStatusAsync(PublishingActivity publishingActivity, Func statusUpdate, CancellationToken cancellationToken); } internal sealed class PublishingActivityProgressReporter : IPublishingActivityProgressReporter { public async Task CreateActivityAsync(string id, string initialStatusText, bool isPrimary, CancellationToken cancellationToken) { - var publishingActivity = new PublishingActivity(id, initialStatusText, isPrimary); - await ActivitiyUpdated.Writer.WriteAsync(publishingActivity, cancellationToken).ConfigureAwait(false); + var publishingActivity = new PublishingActivity(id, isPrimary); + await UpdateActivityStatusAsync( + publishingActivity, + (status) => status with + { + StatusText = initialStatusText, + IsComplete = false, + IsError = false + }, + cancellationToken + ).ConfigureAwait(false); + return publishingActivity; } - public async Task UpdateActivityAsync(PublishingActivity publishingActivity, CancellationToken cancellationToken) + private readonly object _updateLock = new object(); + + public async Task UpdateActivityStatusAsync(PublishingActivity publishingActivity, Func statusUpdate, CancellationToken cancellationToken) { - await ActivitiyUpdated.Writer.WriteAsync(publishingActivity, cancellationToken).ConfigureAwait(false); + PublishingActivityStatus? lastStatus; + PublishingActivityStatus? newStatus; + + lock (_updateLock) + { + lastStatus = publishingActivity.LastStatus ?? new PublishingActivityStatus + { + Activity = publishingActivity, + StatusText = string.Empty, + IsComplete = false, + IsError = false + }; + + newStatus = statusUpdate(lastStatus); + publishingActivity.LastStatus = newStatus; + } + + if (lastStatus == newStatus) + { + throw new DistributedApplicationException( + $"The status of the publishing activity '{publishingActivity.Id}' was not updated. The status update function must return a new instance of the status." + ); + } + + await ActivityStatusUpdated.Writer.WriteAsync(newStatus, cancellationToken).ConfigureAwait(false); - if (publishingActivity.IsPrimary && (publishingActivity.IsComplete || publishingActivity.IsError)) + if (publishingActivity.IsPrimary && (newStatus.IsComplete || newStatus.IsError)) { // If the activity is complete or an error and it is the primary activity, // we can stop listening for updates. - ActivitiyUpdated.Writer.Complete(); + ActivityStatusUpdated.Writer.Complete(); } } - internal Channel ActivitiyUpdated { get; } = Channel.CreateUnbounded(); + internal Channel ActivityStatusUpdated { get; } = Channel.CreateUnbounded(); } \ No newline at end of file diff --git a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs index cd463079792..0ab76d76ce4 100644 --- a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs +++ b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs @@ -130,15 +130,17 @@ private async Task BuildProjectContainerImageAsync(IResource resource, C stdout, stderr); - publishingActivity.IsError = true; - await activityReporter.UpdateActivityAsync(publishingActivity, cancellationToken).ConfigureAwait(false); + await activityReporter.UpdateActivityStatusAsync( + publishingActivity, (status) => status with { IsError = true }, + cancellationToken).ConfigureAwait(false); throw new DistributedApplicationException($"Failed to build container image, stdout: {stdout}, stderr: {stderr}"); } else { - publishingActivity.IsComplete = true; - await activityReporter.UpdateActivityAsync(publishingActivity, cancellationToken).ConfigureAwait(false); + await activityReporter.UpdateActivityStatusAsync( + publishingActivity, (status) => status with { IsComplete = true }, + cancellationToken).ConfigureAwait(false); logger.LogDebug( ".NET CLI completed with exit code: {ExitCode}", @@ -171,8 +173,9 @@ private async Task BuildContainerImageFromDockerfileAsync(string resourc imageName, cancellationToken).ConfigureAwait(false); - publishingActivity.IsComplete = true; - await activityReporter.UpdateActivityAsync(publishingActivity, cancellationToken).ConfigureAwait(false); + await activityReporter.UpdateActivityStatusAsync( + publishingActivity, (status) => status with { IsComplete = true }, + cancellationToken).ConfigureAwait(false); return image; } @@ -180,8 +183,9 @@ private async Task BuildContainerImageFromDockerfileAsync(string resourc { logger.LogError(ex, "Failed to build container image from Dockerfile."); - publishingActivity.IsError = true; - await activityReporter.UpdateActivityAsync(publishingActivity, cancellationToken).ConfigureAwait(false); + await activityReporter.UpdateActivityStatusAsync( + publishingActivity, (status) => status with { IsError = true }, + cancellationToken).ConfigureAwait(false); throw; } From 9080a14360e61c52a8b49057f777caf9eb11498e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:38:02 -0600 Subject: [PATCH 17/30] Rename IAzureKeyVaultResource.GetSecretReference to GetSecret (#8599) Contributes to #7811 Co-authored-by: Eric Erhardt --- src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs | 2 +- src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs | 2 +- src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs | 2 +- src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs | 2 +- .../AzurePostgresFlexibleServerResource.cs | 2 +- src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs | 2 +- src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 8bdeb655259..3d4926022d5 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -370,7 +370,7 @@ public static IResourceBuilder WithAccessKeyAuthenticatio ArgumentNullException.ThrowIfNull(builder); var azureResource = builder.Resource; - azureResource.ConnectionStringSecretOutput = keyVaultBuilder.Resource.GetSecretReference( + azureResource.ConnectionStringSecretOutput = keyVaultBuilder.Resource.GetSecret( $"connectionstrings--{azureResource.Name}"); builder.WithParameter(AzureBicepResource.KnownParameters.KeyVaultName, keyVaultBuilder.Resource.NameOutputReference); diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs index f21cccc6839..96de2468f20 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs @@ -136,7 +136,7 @@ internal ReferenceExpression GetChildConnectionString(string childResourceName, if (UseAccessKeyAuthentication && !IsEmulator) { - builder.AppendFormatted(ConnectionStringSecretOutput.Resource.GetSecretReference(GetKeyValueSecretName(childResourceName))); + builder.AppendFormatted(ConnectionStringSecretOutput.Resource.GetSecret(GetKeyValueSecretName(childResourceName))); } else { diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs index bd6a3580cb8..c059b826777 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs @@ -48,7 +48,7 @@ public class AzureKeyVaultResource(string name, Action /// /// - public IAzureKeyVaultSecretReference GetSecretReference(string secretName) + public IAzureKeyVaultSecretReference GetSecret(string secretName) { ArgumentException.ThrowIfNullOrEmpty(secretName, nameof(secretName)); diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs index 60a279045d7..a53a9051645 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs @@ -333,7 +333,7 @@ public static IResourceBuilder WithPassword ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder.ApplicationBuilder, $"{builder.Resource.Name}-password"); builder.WithParameter("administratorLoginPassword", azureResource.PasswordParameter); - azureResource.ConnectionStringSecretOutput = keyVaultBuilder.Resource.GetSecretReference($"connectionstrings--{builder.Resource.Name}"); + azureResource.ConnectionStringSecretOutput = keyVaultBuilder.Resource.GetSecret($"connectionstrings--{builder.Resource.Name}"); builder.WithParameter(AzureBicepResource.KnownParameters.KeyVaultName, keyVaultBuilder.Resource.NameOutputReference); diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs index 0946756806c..a18436c2320 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs @@ -99,7 +99,7 @@ internal ReferenceExpression GetDatabaseConnectionString(string databaseResource // Note that the bicep template puts each database's connection string in a KeyVault secret. if (InnerResource is null && ConnectionStringSecretOutput is not null) { - return ReferenceExpression.Create($"{ConnectionStringSecretOutput.Resource.GetSecretReference(GetDatabaseKeyVaultSecretName(databaseResourceName))}"); + return ReferenceExpression.Create($"{ConnectionStringSecretOutput.Resource.GetSecret(GetDatabaseKeyVaultSecretName(databaseResourceName))}"); } return ReferenceExpression.Create($"{this};Database={databaseName}"); diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs index a0171036463..99b22c3c9fc 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs @@ -224,7 +224,7 @@ public static IResourceBuilder WithAccessKeyAuthenticat ArgumentNullException.ThrowIfNull(keyVaultBuilder); var azureResource = builder.Resource; - azureResource.ConnectionStringSecretOutput = keyVaultBuilder.Resource.GetSecretReference($"connectionstrings--{azureResource.Name}"); + azureResource.ConnectionStringSecretOutput = keyVaultBuilder.Resource.GetSecret($"connectionstrings--{azureResource.Name}"); builder.WithParameter(AzureBicepResource.KnownParameters.KeyVaultName, keyVaultBuilder.Resource.NameOutputReference); // remove role assignment annotations when using access key authentication so an empty roles bicep module isn't generated diff --git a/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs index 432b072dcf1..7c33a936583 100644 --- a/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs @@ -30,5 +30,5 @@ public interface IAzureKeyVaultResource : IResource, IAzureResource /// /// The name of the secret. /// A reference to the secret. - IAzureKeyVaultSecretReference GetSecretReference(string secretName); + IAzureKeyVaultSecretReference GetSecret(string secretName); } From 97a83d344e53ce890d76bd781ea7dfe13acc6497 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:11:12 -0700 Subject: [PATCH 18/30] [release/9.2] Address feedback to WithUrls() (#8605) * Address WithUrls feedback Fixes #8587 * Add more WithUrl tests * More tests * Fix test failure * Fix test * PR feedback --------- Co-authored-by: Damian Edwards --- .../Dcp/ResourceSnapshotBuilder.cs | 21 ++- .../Orchestrator/ApplicationOrchestrator.cs | 67 +++++--- .../ResourceBuilderExtensions.cs | 53 +++++- .../DistributedApplicationTests.cs | 50 +++--- tests/Aspire.Hosting.Tests/WithUrlsTests.cs | 160 +++++++++++++++++- 5 files changed, 289 insertions(+), 62 deletions(-) diff --git a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs index ab30fcad744..2a59c60a329 100644 --- a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs +++ b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs @@ -20,7 +20,7 @@ public ResourceSnapshotBuilder(DcpResourceState resourceState) public CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSnapshot previous) { var containerId = container.Status?.ContainerId; - var urls = GetUrls(container); + var urls = GetUrls(container, container.Status?.State); var volumes = GetVolumes(container); var environment = GetEnvironmentVariables(container.Status?.EffectiveEnv ?? container.Spec.Env, container.Spec.Env); @@ -99,7 +99,7 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn var state = executable.AppModelInitialState is "Hidden" ? "Hidden" : executable.Status?.State; - var urls = GetUrls(executable); + var urls = GetUrls(executable, executable.Status?.State); var environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env); @@ -183,7 +183,7 @@ private static (ImmutableArray Args, ImmutableArray? ArgsAreSensiti return (launchArgsBuilder.ToImmutable(), argsAreSensitiveBuilder.ToImmutable(), anySensitive); } - private ImmutableArray GetUrls(CustomResource resource) + private ImmutableArray GetUrls(CustomResource resource, string? resourceState) { var urls = ImmutableArray.CreateBuilder(); var appModelResourceName = resource.AppModelResourceName; @@ -199,21 +199,26 @@ private ImmutableArray GetUrls(CustomResource resource) var name = resource.Metadata.Name; // Add the endpoint URLs - foreach (var service in resourceServices) + var serviceEndpoints = new HashSet<(string EndpointName, string ServiceMetadataName)>(resourceServices.Where(s => !string.IsNullOrEmpty(s.EndpointName)).Select(s => (s.EndpointName!, s.Metadata.Name))); + foreach (var endpoint in serviceEndpoints) { - if (endpointUrls.FirstOrDefault(u => string.Equals(service.EndpointName, u.Endpoint?.EndpointName, StringComparisons.EndpointAnnotationName)) is { Endpoint: { } } endpointUrl) + var (endpointName, serviceName) = endpoint; + var urlsForEndpoint = endpointUrls.Where(u => string.Equals(endpointName, u.Endpoint?.EndpointName, StringComparisons.EndpointAnnotationName)).ToList(); + + foreach (var endpointUrl in urlsForEndpoint) { - var activeEndpoint = _resourceState.EndpointsMap.SingleOrDefault(e => e.Value.Spec.ServiceName == service.Metadata.Name && e.Value.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) == true).Value; + var activeEndpoint = _resourceState.EndpointsMap.SingleOrDefault(e => e.Value.Spec.ServiceName == serviceName && e.Value.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) == true).Value; var isInactive = activeEndpoint is null; - urls.Add(new(Name: endpointUrl.Endpoint.EndpointName, Url: endpointUrl.Url, IsInternal: false) { IsInactive = isInactive, DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0) }); + urls.Add(new(Name: endpointUrl.Endpoint!.EndpointName, Url: endpointUrl.Url, IsInternal: false) { IsInactive = isInactive, DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0) }); } } // Add the non-endpoint URLs + var resourceRunning = string.Equals(resourceState, KnownResourceStates.Running, StringComparisons.ResourceState); foreach (var url in nonEndpointUrls) { - urls.Add(new(Name: null, Url: url.Url, IsInternal: false) { IsInactive = false, DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0) }); + urls.Add(new(Name: null, Url: url.Url, IsInternal: false) { IsInactive = !resourceRunning, DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0) }); } } diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 50515cbbedb..b967a592a3d 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -97,12 +97,13 @@ private async Task OnEndpointsAllocated(OnEndpointsAllocatedContext context) { await lifecycleHook.AfterEndpointsAllocatedAsync(_model, context.CancellationToken).ConfigureAwait(false); } - - await ProcessUrls(context.CancellationToken).ConfigureAwait(false); } private async Task OnResourceStarting(OnResourceStartingContext context) { + // Call the callbacks to configure resource URLs + await ProcessUrls(context.Resource, context.CancellationToken).ConfigureAwait(false); + switch (context.ResourceType) { case KnownResourceTypes.Project: @@ -152,44 +153,58 @@ private async Task OnResourcesPrepared(OnResourcesPreparedContext _) await PublishResourcesWithInitialStateAsync().ConfigureAwait(false); } - private async Task ProcessUrls(CancellationToken cancellationToken) + private async Task ProcessUrls(IResource resource, CancellationToken cancellationToken) { - // Project endpoints to URLS - foreach (var resource in _model.Resources.OfType()) + if (resource is not IResourceWithEndpoints resourceWithEndpoints) { - var urls = new List(); + return; + } - if (resource.TryGetEndpoints(out var endpoints)) + // Project endpoints to URLS + var urls = new List(); + + if (resource.TryGetEndpoints(out var endpoints)) + { + foreach (var endpoint in endpoints) { - foreach (var endpoint in endpoints) + // Create a URL for each endpoint + if (endpoint.AllocatedEndpoint is { } allocatedEndpoint) { - // Create a URL for each endpoint - if (endpoint.AllocatedEndpoint is { } allocatedEndpoint) - { - var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = new EndpointReference(resource, endpoint) }; - urls.Add(url); - } + var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = new EndpointReference(resourceWithEndpoints, endpoint) }; + urls.Add(url); } } + } - // Run the URL callbacks - if (resource.TryGetAnnotationsOfType(out var callbacks)) + // Run the URL callbacks + if (resource.TryGetAnnotationsOfType(out var callbacks)) + { + var urlsCallbackContext = new ResourceUrlsCallbackContext(new(DistributedApplicationOperation.Run), resource, urls, cancellationToken) { - var urlsCallbackContext = new ResourceUrlsCallbackContext(new(DistributedApplicationOperation.Run), resource, urls, cancellationToken) - { - Logger = _loggerService.GetLogger(resource.Name) - }; - foreach (var callback in callbacks) - { - await callback.Callback(urlsCallbackContext).ConfigureAwait(false); - } + Logger = _loggerService.GetLogger(resource.Name) + }; + foreach (var callback in callbacks) + { + await callback.Callback(urlsCallbackContext).ConfigureAwait(false); } + } - foreach (var url in urls) + // Clear existing URLs + if (resource.TryGetUrls(out var existingUrls)) + { + var existing = existingUrls.ToArray(); + for (var i = existing.Length - 1; i >= 0; i--) { - resource.Annotations.Add(url); + var url = existing[i]; + resource.Annotations.Remove(url); } } + + // Add URLs + foreach (var url in urls) + { + resource.Annotations.Add(url); + } } private Task ProcessResourcesWithoutLifetime(AfterEndpointsAllocatedEvent @event, CancellationToken cancellationToken) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index d520afb77c7..81fcc1a5c6c 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -799,7 +799,58 @@ public static IResourceBuilder WithUrl(this IResourceBuilder builder, s } /// - /// Registers a callback to customize the URL displayed for the endpoint with the specified name. + /// Adds a URL to be displayed for the resource. + /// + /// The resource type. + /// The builder for the resource. + /// The interpolated string that produces the URL. + /// The display text to show when the link is displayed. + /// The . + /// + /// Use this method to add a URL to be displayed for the resource.
+ /// Note that any endpoints on the resource will automatically get a corresponding URL added for them. + ///
+ public static IResourceBuilder WithUrl(this IResourceBuilder builder, in ReferenceExpression.ExpressionInterpolatedStringHandler url, string? displayText = null) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + + var expression = url.GetExpression(); + + return builder.WithUrl(expression, displayText); + } + + /// + /// Adds a URL to be displayed for the resource. + /// + /// The resource type. + /// The builder for the resource. + /// A that will produce the URL. + /// The display text to show when the link is displayed. + /// The . + /// + /// Use this method to add a URL to be displayed for the resource.
+ /// Note that any endpoints on the resource will automatically get a corresponding URL added for them. + ///
+ public static IResourceBuilder WithUrl(this IResourceBuilder builder, ReferenceExpression url, string? displayText = null) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(url); + + return builder.WithAnnotation(new ResourceUrlsCallbackAnnotation(async c => + { + var endpoint = url.ValueProviders.OfType().FirstOrDefault(); + var urlValue = await url.GetValueAsync(c.CancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(urlValue)) + { + c.Urls.Add(new() { Endpoint = endpoint, Url = urlValue, DisplayText = displayText }); + } + })); + } + + /// + /// Registers a callback to update the URL displayed for the endpoint with the specified name. /// /// The resource type. /// The builder for the resource. diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 13c266721ca..fe627c6a35a 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -177,26 +177,26 @@ public async Task ExplicitStart_StartExecutable() var notStartedResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.NotStarted).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); var dependentResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Waiting).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); - // Inactive URLs and source should be populated on non-started resources. + // Source should be populated on non-started resources. Assert.Contains("TestProject.ServiceA.csproj", notStartedResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString()); - Assert.Collection(notStartedResourceEvent.Snapshot.Urls, u => - { - Assert.Equal("http://localhost:5156", u.Url); - Assert.True(u.IsInactive); - }); Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString()); - Assert.Collection(dependentResourceEvent.Snapshot.Urls, u => - { - Assert.Equal("http://localhost:5254", u.Url); - Assert.True(u.IsInactive); - }); logger.LogInformation("Start explicit start resource."); await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); - await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + var runningResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + Assert.Collection(runningResourceEvent.Snapshot.Urls, u => + { + Assert.Equal("http://localhost:5156", u.Url); + Assert.Equal("http", u.Name); + }); // Dependent resource should now run. - await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + var dependentResourceRunningEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + Assert.Collection(dependentResourceRunningEvent.Snapshot.Urls, u => + { + Assert.Equal("http://localhost:5254", u.Url); + Assert.Equal("http", u.Name); + }); logger.LogInformation("Stop resource."); await orchestrator.StopResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); @@ -239,27 +239,27 @@ public async Task ExplicitStart_StartContainer() var notStartedResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.NotStarted).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); var dependentResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Waiting).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); - // Inactive URLs and source should be populated on non-started resources. + // Source should be populated on non-started resources. Assert.Equal(RedisImageSource, notStartedResourceEvent.Snapshot.Properties.Single(p => p.Name == "container.image").Value?.ToString()); - Assert.Collection(notStartedResourceEvent.Snapshot.Urls, u => + Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString()); + + logger.LogInformation("Start explicit start resource."); + await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + var runningResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + Assert.Collection(runningResourceEvent.Snapshot.Urls, u => { Assert.Equal("tcp://localhost:6379", u.Url); Assert.True(u.IsInactive); }); - Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString()); - Assert.Collection(dependentResourceEvent.Snapshot.Urls, u => + + // Dependent resource should now run. + var dependentRunningResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + Assert.Collection(dependentRunningResourceEvent.Snapshot.Urls, u => { Assert.Equal("http://localhost:5254", u.Url); - Assert.True(u.IsInactive); + Assert.Equal("http", u.Name); }); - logger.LogInformation("Start explicit start resource."); - await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); - await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); - - // Dependent resource should now run. - await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); - logger.LogInformation("Stop resource."); await orchestrator.StopResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Exited).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); diff --git a/tests/Aspire.Hosting.Tests/WithUrlsTests.cs b/tests/Aspire.Hosting.Tests/WithUrlsTests.cs index b20c3e5bb80..b6cb59bcbac 100644 --- a/tests/Aspire.Hosting.Tests/WithUrlsTests.cs +++ b/tests/Aspire.Hosting.Tests/WithUrlsTests.cs @@ -1,7 +1,10 @@ // 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; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -41,7 +44,7 @@ public void WithUrlsAddsAnnotationForSyncCallback() } [Fact] - public async Task WithUrlsCallsCallbackAfterEndpointsAllocated() + public async Task WithUrlsCallsCallbackAfterBeforeResourceStartedEvent() { using var builder = TestDistributedApplicationBuilder.Create(); @@ -52,7 +55,7 @@ public async Task WithUrlsCallsCallbackAfterEndpointsAllocated() var tcs = new TaskCompletionSource(); builder.Eventing.Subscribe((e, ct) => { - // Should not be called until after event handlers for AfterEndpointsAllocatedEvent + // Should not be called at this point Assert.False(called); return Task.CompletedTask; }); @@ -149,6 +152,36 @@ public async Task WithUrlAddsUrlAnnotation() await app.StopAsync(); } + [Fact] + public async Task WithUrlInterpolatedStringAddsUrlAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var projectA = builder.AddProject("projecta") + .WithHttpsEndpoint(); + projectA.WithUrl($"{projectA.Resource.GetEndpoint("https")}/test", "Example"); + + var tcs = new TaskCompletionSource(); + builder.Eventing.Subscribe(projectA.Resource, (e, ct) => + { + tcs.SetResult(); + return Task.CompletedTask; + }); + + var app = await builder.BuildAsync(); + await app.StartAsync(); + await tcs.Task; + + var urls = projectA.Resource.Annotations.OfType(); + var endpointUrl = urls.First(u => u.Endpoint is not null); + Assert.Collection(urls, + u => Assert.True(u.Url == endpointUrl.Url && u.DisplayText is null), + u => Assert.True(u.Url.EndsWith("/test") && u.DisplayText == "Example") + ); + + await app.StopAsync(); + } + [Fact] public async Task EndpointsResultInUrls() { @@ -257,6 +290,129 @@ public async Task WithUrlForEndpointUpdatesUrlForEndpoint() await app.StopAsync(); } + [Fact] + public async Task EndpointUrlsAreInitiallyInactive() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var servicea = builder.AddProject("servicea") + .WithUrlForEndpoint("http", u => u.Url = "https://example.com"); + + var httpEndpoint = servicea.Resource.GetEndpoint("http"); + + var app = await builder.BuildAsync(); + var rns = app.Services.GetRequiredService(); + ImmutableArray initialUrlSnapshot = default; + var cts = new CancellationTokenSource(); + var watchTask = Task.Run(async () => + { + await foreach (var notification in rns.WatchAsync(cts.Token).WithCancellation(cts.Token)) + { + if (notification.Snapshot.Urls.Length > 0 && initialUrlSnapshot == default) + { + initialUrlSnapshot = notification.Snapshot.Urls; + break; + } + } + }); + + await app.StartAsync(); + + await watchTask.DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + cts.Cancel(); + + await app.StopAsync(); + + Assert.Single(initialUrlSnapshot, s => s.Name == httpEndpoint.EndpointName && s.IsInactive && s.Url == "https://example.com"); + } + + [Fact] + public async Task MultipleUrlsForSingleEndpointAreIncludedInUrlSnapshot() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var servicea = builder.AddProject("servicea"); + var httpEndpoint = servicea.Resource.GetEndpoint("http"); + servicea.WithUrl($"{httpEndpoint}/one", "Example 1"); + servicea.WithUrl($"{httpEndpoint}/two", "Example 2"); + + var app = await builder.BuildAsync(); + var rns = app.Services.GetRequiredService(); + ImmutableArray initialUrlSnapshot = default; + var cts = new CancellationTokenSource(); + var watchTask = Task.Run(async () => + { + await foreach (var notification in rns.WatchAsync(cts.Token).WithCancellation(cts.Token)) + { + if (notification.Snapshot.Urls.Length > 0 && initialUrlSnapshot == default) + { + initialUrlSnapshot = notification.Snapshot.Urls; + break; + } + } + }); + + await app.StartAsync(); + + await watchTask.DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + cts.Cancel(); + + await app.StopAsync(); + + Assert.Collection(initialUrlSnapshot, + s => Assert.True(s.Name == httpEndpoint.EndpointName && s.DisplayProperties.DisplayName == ""), // <-- this is the default URL added for the endpoint + s => Assert.True(s.Name == httpEndpoint.EndpointName && s.Url.EndsWith("/one") && s.DisplayProperties.DisplayName == "Example 1"), + s => Assert.True(s.Name == httpEndpoint.EndpointName && s.Url.EndsWith("/two") && s.DisplayProperties.DisplayName == "Example 2") + ); + } + + [Fact] + public async Task NonEndpointUrlsAreInactiveUntilResourceRunning() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddProject("servicea") + .WithUrl("https://example.com"); + + var app = await builder.BuildAsync(); + + var rns = app.Services.GetRequiredService(); + ImmutableArray initialUrlSnapshot = default; + ImmutableArray urlSnapshotAfterRunning = default; + var cts = new CancellationTokenSource(); + var watchTask = Task.Run(async () => + { + await foreach (var notification in rns.WatchAsync(cts.Token).WithCancellation(cts.Token)) + { + if (notification.Snapshot.Urls.Length > 0 && initialUrlSnapshot == default) + { + initialUrlSnapshot = notification.Snapshot.Urls; + continue; + } + + if (string.Equals(notification.Snapshot.State?.Text, KnownResourceStates.Running)) + { + if (notification.Snapshot.Urls.Length > 0 && urlSnapshotAfterRunning == default) + { + urlSnapshotAfterRunning = notification.Snapshot.Urls; + break; + } + } + } + }); + + await app.StartAsync(); + + await rns.WaitForResourceAsync("servicea", KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + await watchTask.DefaultTimeout(TestConstants.LongTimeoutTimeSpan); + cts.Cancel(); + + await app.StopAsync(); + + Assert.All(initialUrlSnapshot, s => Assert.True(s.IsInactive)); + Assert.Single(urlSnapshotAfterRunning, s => !s.IsInactive && s.Url == "https://example.com"); + } + [Fact] public async Task WithUrlForEndpointDoesNotThrowOrCallCallbackIfEndpointNotFound() { From 0623631fb7902ae632b0e534cec559bfcaa3e451 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 8 Apr 2025 11:19:11 +1000 Subject: [PATCH 19/30] [release/9.2] Add RPC protocol compat check. (#8604) * Add RPC protocol compat check. (#8577) * Add RPC protocol compat check. * Fix merge conflict. * Fix spelling. * Update DotNetCliRunner.cs Co-authored-by: David Fowler * Improve error message with version info. --------- Co-authored-by: David Fowler * Fix --watch hangs. (#8585) * Fix --watch hangs. * Don't prebuild in watch mode. * Fix up merge. * Add watch/no-build conflict fix. * Fix spelling. * Spelling. --------- Co-authored-by: David Fowler --- .../Backchannel/AppHostBackchannel.cs | 29 ++ .../AppHostIncompatibleException.cs | 9 + src/Aspire.Cli/Commands/NewCommand.cs | 8 +- src/Aspire.Cli/Commands/PublishCommand.cs | 354 +++++++++--------- src/Aspire.Cli/Commands/RunCommand.cs | 286 +++++++------- src/Aspire.Cli/DotNetCliRunner.cs | 20 +- src/Aspire.Cli/ExitCodeConstants.cs | 1 + src/Aspire.Cli/Utils/AppHostHelper.cs | 12 +- .../{PromptUtils.cs => InteractionUtils.cs} | 16 +- .../Backchannel/AppHostRpcTarget.cs | 27 ++ 10 files changed, 443 insertions(+), 319 deletions(-) create mode 100644 src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs rename src/Aspire.Cli/Utils/{PromptUtils.cs => InteractionUtils.cs} (66%) diff --git a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs index 7558ea489b4..c605bcf02e0 100644 --- a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs @@ -106,6 +106,19 @@ public async Task ConnectAsync(Process process, string socketPath, CancellationT var stream = new NetworkStream(socket, true); var rpc = JsonRpc.Attach(stream, target); + var capabilities = await rpc.InvokeWithCancellationAsync( + "GetCapabilitiesAsync", + Array.Empty(), + cancellationToken); + + if (!capabilities.Any(s => s == "baseline.v0")) + { + throw new AppHostIncompatibleException( + $"AppHost is incompatible with the CLI. The AppHost must be updated to a version that supports the baseline.v0 capability.", + "baseline.v0" + ); + } + _rpcTaskCompletionSource.SetResult(rpc); } @@ -145,4 +158,20 @@ public async Task GetPublishersAsync(CancellationToken cancellationTok yield return state; } } + + public async Task GetCapabilitiesAsync(CancellationToken cancellationToken) + { + using var activity = _activitySource.StartActivity(); + + var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false); + + logger.LogDebug("Requesting capabilities"); + + var capabilities = await rpc.InvokeWithCancellationAsync( + "GetCapabilitiesAsync", + Array.Empty(), + cancellationToken).ConfigureAwait(false); + + return capabilities; + } } diff --git a/src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs b/src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs new file mode 100644 index 00000000000..29f9624ddc6 --- /dev/null +++ b/src/Aspire.Cli/Backchannel/AppHostIncompatibleException.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.Cli.Backchannel; + +internal sealed class AppHostIncompatibleException(string message, string requiredCapability) : Exception(message) +{ + public string RequiredCapability { get; } = requiredCapability; +} \ No newline at end of file diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 2e095073117..2b81943a465 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -70,7 +70,7 @@ public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) } else { - return await PromptUtils.PromptForSelectionAsync( + return await InteractionUtils.PromptForSelectionAsync( "Select a project template:", validTemplates, t => $"{t.TemplateName} ({t.TemplateDescription})", @@ -84,7 +84,7 @@ private static async Task GetProjectNameAsync(ParseResult parseResult, C if (parseResult.GetValue("--name") is not { } name) { var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name; - name = await PromptUtils.PromptForStringAsync("Enter the project name:", + name = await InteractionUtils.PromptForStringAsync("Enter the project name:", defaultValue: defaultName, cancellationToken: cancellationToken); } @@ -96,7 +96,7 @@ private static async Task GetOutputPathAsync(ParseResult parseResult, st { if (parseResult.GetValue("--output") is not { } outputPath) { - outputPath = await PromptUtils.PromptForStringAsync( + outputPath = await InteractionUtils.PromptForStringAsync( "Enter the output path:", defaultValue: pathAppendage ?? ".", cancellationToken: cancellationToken @@ -114,7 +114,7 @@ private static async Task GetProjectTemplatesVersionAsync(ParseResult pa } else { - version = await PromptUtils.PromptForStringAsync( + version = await InteractionUtils.PromptForStringAsync( "Project templates version:", defaultValue: VersionHelper.GetDefaultTemplateVersion(), validator: (string value) => { diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index cce35f56d32..4ac12ef9e2e 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -38,213 +38,225 @@ public PublishCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(); + (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null; - var passedAppHostProjectFile = parseResult.GetValue("--project"); - var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); - - if (effectiveAppHostProjectFile is null) + try { - return ExitCodeConstants.FailedToFindProject; - } - - var env = new Dictionary(); - - if (parseResult.GetValue("--wait-for-debugger") ?? false) - { - env[KnownConfigNames.WaitForDebugger] = "true"; - } - - var appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - - if (!appHostCompatabilityCheck.IsCompatableAppHost) - { - return ExitCodeConstants.FailedToDotnetRunAppHost; - } - - var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - - if (buildExitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } - - var publisher = parseResult.GetValue("--publisher"); - var outputPath = parseResult.GetValue("--output-path"); - var fullyQualifiedOutputPath = Path.GetFullPath(outputPath ?? "."); + using var activity = _activitySource.StartActivity(); - var publishersResult = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync<(int ExitCode, string[]? Publishers)>( - publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...", - async context => { - - using var getPublishersActivity = _activitySource.StartActivity( - $"{nameof(ExecuteAsync)}-Action-GetPublishers", - ActivityKind.Client); + var passedAppHostProjectFile = parseResult.GetValue("--project"); + var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); + + if (effectiveAppHostProjectFile is null) + { + return ExitCodeConstants.FailedToFindProject; + } - var backchannelCompletionSource = new TaskCompletionSource(); - var pendingInspectRun = _runner.RunAsync( - effectiveAppHostProjectFile, - false, - true, - ["--operation", "inspect"], - null, - backchannelCompletionSource, - cancellationToken).ConfigureAwait(false); + var env = new Dictionary(); - var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); - var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false); - - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - var exitCode = await pendingInspectRun; + if (parseResult.GetValue("--wait-for-debugger") ?? false) + { + env[KnownConfigNames.WaitForDebugger] = "true"; + } - return (exitCode, publishers); + appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - }).ConfigureAwait(false); + if (!appHostCompatibilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) + { + return ExitCodeConstants.FailedToDotnetRunAppHost; + } - if (publishersResult.ExitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The publisher inspection failed with exit code {publishersResult.ExitCode}. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - var publishers = publishersResult.Publishers; - if (publishers is null || publishers.Length == 0) - { - AnsiConsole.MarkupLine("[red bold]:thumbs_down: No publishers were found.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } + if (buildExitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } - if (publishers?.Contains(publisher) != true) - { - if (publisher is not null) + var publisher = parseResult.GetValue("--publisher"); + var outputPath = parseResult.GetValue("--output-path"); + var fullyQualifiedOutputPath = Path.GetFullPath(outputPath ?? "."); + + var publishersResult = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync<(int ExitCode, string[]? Publishers)>( + publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...", + async context => { + + using var getPublishersActivity = _activitySource.StartActivity( + $"{nameof(ExecuteAsync)}-Action-GetPublishers", + ActivityKind.Client); + + var backchannelCompletionSource = new TaskCompletionSource(); + var pendingInspectRun = _runner.RunAsync( + effectiveAppHostProjectFile, + false, + true, + ["--operation", "inspect"], + null, + backchannelCompletionSource, + cancellationToken).ConfigureAwait(false); + + var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false); + + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingInspectRun; + + return (exitCode, publishers); + + }).ConfigureAwait(false); + + if (publishersResult.ExitCode != 0) { - AnsiConsole.MarkupLine($"[red bold]:warning: The specified publisher '{publisher}' was not found.[/]"); + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The publisher inspection failed with exit code {publishersResult.ExitCode}. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; } - var publisherPrompt = new SelectionPrompt() - .Title("Select a publisher:") - .UseConverter(p => p) - .PageSize(10) - .EnableSearch() - .HighlightStyle(Style.Parse("darkmagenta")) - .AddChoices(publishers!); + var publishers = publishersResult.Publishers; + if (publishers is null || publishers.Length == 0) + { + AnsiConsole.MarkupLine("[red bold]:thumbs_down: No publishers were found.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } - publisher = await AnsiConsole.PromptAsync(publisherPrompt, cancellationToken); - } + if (publishers?.Contains(publisher) != true) + { + if (publisher is not null) + { + AnsiConsole.MarkupLine($"[red bold]:warning: The specified publisher '{publisher}' was not found.[/]"); + } - AnsiConsole.MarkupLine($":hammer_and_wrench: Generating artifacts for '{publisher}' publisher..."); + var publisherPrompt = new SelectionPrompt() + .Title("Select a publisher:") + .UseConverter(p => p) + .PageSize(10) + .EnableSearch() + .HighlightStyle(Style.Parse("darkmagenta")) + .AddChoices(publishers!); - var exitCode = await AnsiConsole.Progress() - .AutoRefresh(true) - .Columns( - new TaskDescriptionColumn() { Alignment = Justify.Left }, - new ProgressBarColumn() { Width = 10 }, - new ElapsedTimeColumn()) - .StartAsync(async context => { + publisher = await AnsiConsole.PromptAsync(publisherPrompt, cancellationToken); + } - using var generateArtifactsActivity = _activitySource.StartActivity( - $"{nameof(ExecuteAsync)}-Action-GenerateArtifacts", - ActivityKind.Internal); - - var backchannelCompletionSource = new TaskCompletionSource(); + AnsiConsole.MarkupLine($":hammer_and_wrench: Generating artifacts for '{publisher}' publisher..."); - var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); - launchingAppHostTask.IsIndeterminate(); - launchingAppHostTask.StartTask(); + var exitCode = await AnsiConsole.Progress() + .AutoRefresh(true) + .Columns( + new TaskDescriptionColumn() { Alignment = Justify.Left }, + new ProgressBarColumn() { Width = 10 }, + new ElapsedTimeColumn()) + .StartAsync(async context => { - var pendingRun = _runner.RunAsync( - effectiveAppHostProjectFile, - false, - true, - ["--publisher", publisher ?? "manifest", "--output-path", fullyQualifiedOutputPath], - env, - backchannelCompletionSource, - cancellationToken); + using var generateArtifactsActivity = _activitySource.StartActivity( + $"{nameof(ExecuteAsync)}-Action-GenerateArtifacts", + ActivityKind.Internal); + + var backchannelCompletionSource = new TaskCompletionSource(); - var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); + launchingAppHostTask.IsIndeterminate(); + launchingAppHostTask.StartTask(); - launchingAppHostTask.Description = $":check_mark: Launching apphost"; - launchingAppHostTask.Value = 100; - launchingAppHostTask.StopTask(); + var pendingRun = _runner.RunAsync( + effectiveAppHostProjectFile, + false, + true, + ["--publisher", publisher ?? "manifest", "--output-path", fullyQualifiedOutputPath], + env, + backchannelCompletionSource, + cancellationToken); - var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken); + var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); - var progressTasks = new Dictionary(); + launchingAppHostTask.Description = $":check_mark: Launching apphost"; + launchingAppHostTask.Value = 100; + launchingAppHostTask.StopTask(); - await foreach (var publishingActivity in publishingActivities) - { - if (!progressTasks.TryGetValue(publishingActivity.Id, out var progressTask)) - { - progressTask = context.AddTask(publishingActivity.Id); - progressTask.StartTask(); - progressTask.IsIndeterminate(); - progressTasks.Add(publishingActivity.Id, progressTask); - } + var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken); - progressTask.Description = $":play_button: {publishingActivity.StatusText}"; + var progressTasks = new Dictionary(); - if (publishingActivity.IsComplete && !publishingActivity.IsError) + await foreach (var publishingActivity in publishingActivities) { - progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; - progressTask.Value = 100; - progressTask.StopTask(); + if (!progressTasks.TryGetValue(publishingActivity.Id, out var progressTask)) + { + progressTask = context.AddTask(publishingActivity.Id); + progressTask.StartTask(); + progressTask.IsIndeterminate(); + progressTasks.Add(publishingActivity.Id, progressTask); + } + + progressTask.Description = $":play_button: {publishingActivity.StatusText}"; + + if (publishingActivity.IsComplete && !publishingActivity.IsError) + { + progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; + progressTask.Value = 100; + progressTask.StopTask(); + } + else if (publishingActivity.IsError) + { + progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; + progressTask.Value = 0; + break; + } + else + { + // Keep going man! + } } - else if (publishingActivity.IsError) + + // When we are running in publish mode we don't want the app host to + // stop itself while we might still be streaming data back across + // the RPC backchannel. So we need to take responsibility for stopping + // the app host. If the CLI exits/crashes without explicitly stopping + // the app host the orphan detector in the app host will kick in. + if (progressTasks.Any(kvp => !kvp.Value.IsFinished)) { - progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; - progressTask.Value = 0; - break; + // Depending on the failure the publisher may return a zero + // exit code. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + + // If we are in the state where we've detected an error because there + // is an incomplete task then we stop the app host, but depending on + // where/how the failure occured, we might still get a zero exit + // code. If we get a non-zero exit code we want to return that + // as it might be useful for diagnostic purposes, however if we don't + // get a non-zero exit code we want to return our built-in exit code + // for failed artifact build. + return exitCode == 0 ? ExitCodeConstants.FailedToBuildArtifacts : exitCode; } else { - // Keep going man! + // If we are here then all the tasks are finished and we can + // stop the app host. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + return exitCode; // should be zero for orderly shutdown but we pass it along anyway. } - } - - // When we are running in publish mode we don't want the app host to - // stop itself while we might still be streaming data back across - // the RPC backchannel. So we need to take responsibility for stopping - // the app host. If the CLI exits/crashes without explicitly stopping - // the app host the orphan detector in the app host will kick in. - if (progressTasks.Any(kvp => !kvp.Value.IsFinished)) - { - // Depending on the failure the publisher may return a zero - // exit code. - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - var exitCode = await pendingRun; - - // If we are in the state where we've detected an error because there - // is an incomplete task then we stop the app host, but depending on - // where/how the failure occured, we might still get a zero exit - // code. If we get a non-zero exit code we want to return that - // as it might be useful for diagnostic purposes, however if we don't - // get a non-zero exit code we want to return our built-in exit code - // for failed artifact build. - return exitCode == 0 ? ExitCodeConstants.FailedToBuildArtifacts : exitCode; - } - else - { - // If we are here then all the tasks are finished and we can - // stop the app host. - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - var exitCode = await pendingRun; - return exitCode; // should be zero for orderly shutdown but we pass it along anyway. - } - }); + }); - if (exitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Publishing artifacts failed with exit code {exitCode}. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; + if (exitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Publishing artifacts failed with exit code {exitCode}. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } + else + { + AnsiConsole.MarkupLine($"[green bold]:thumbs_up: Successfully published artifacts to: {fullyQualifiedOutputPath}[/]"); + return ExitCodeConstants.Success; + } } - else + catch (AppHostIncompatibleException ex) { - AnsiConsole.MarkupLine($"[green bold]:thumbs_up: Successfully published artifacts to: {fullyQualifiedOutputPath}[/]"); - return ExitCodeConstants.Success; + return InteractionUtils.DisplayIncompatibleVersionError( + ex, + appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + ); } } } diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index b7d9b1e70c1..257808bef09 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -36,175 +36,189 @@ public RunCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(); - - var passedAppHostProjectFile = parseResult.GetValue("--project"); - var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); - - if (effectiveAppHostProjectFile is null) + (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null; + try { - return ExitCodeConstants.FailedToFindProject; - } + using var activity = _activitySource.StartActivity(); - var env = new Dictionary(); + var passedAppHostProjectFile = parseResult.GetValue("--project"); + var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); + + if (effectiveAppHostProjectFile is null) + { + return ExitCodeConstants.FailedToFindProject; + } - var debug = parseResult.GetValue("--debug"); + var env = new Dictionary(); - var waitForDebugger = parseResult.GetValue("--wait-for-debugger"); + var debug = parseResult.GetValue("--debug"); - var forceUseRichConsole = Environment.GetEnvironmentVariable(KnownConfigNames.ForceRichConsole) == "true"; - - var useRichConsole = forceUseRichConsole || !debug && !waitForDebugger; + var waitForDebugger = parseResult.GetValue("--wait-for-debugger"); - if (waitForDebugger) - { - env[KnownConfigNames.WaitForDebugger] = "true"; - } + var forceUseRichConsole = Environment.GetEnvironmentVariable(KnownConfigNames.ForceRichConsole) == "true"; + + var useRichConsole = forceUseRichConsole || !debug && !waitForDebugger; - try - { - await CertificatesHelper.EnsureCertificatesTrustedAsync(_runner, cancellationToken); - } - catch (Exception ex) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: An error occurred while trusting the certificates: {ex.Message}[/]"); - return ExitCodeConstants.FailedToTrustCertificates; - } + if (waitForDebugger) + { + env[KnownConfigNames.WaitForDebugger] = "true"; + } - var appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + try + { + await CertificatesHelper.EnsureCertificatesTrustedAsync(_runner, cancellationToken); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: An error occurred while trusting the certificates: {ex.Message}[/]"); + return ExitCodeConstants.FailedToTrustCertificates; + } - if (!appHostCompatabilityCheck.IsCompatableAppHost) - { - return ExitCodeConstants.FailedToDotnetRunAppHost; - } + appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - var watch = parseResult.GetValue("--watch"); + if (!appHostCompatibilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) + { + return ExitCodeConstants.FailedToDotnetRunAppHost; + } - var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + var watch = parseResult.GetValue("--watch"); - if (buildExitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } + if (!watch) + { + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - var backchannelCompletitionSource = new TaskCompletionSource(); + if (buildExitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } + } - var pendingRun = _runner.RunAsync( - effectiveAppHostProjectFile, - watch, - true, - Array.Empty(), - env, - backchannelCompletitionSource, - cancellationToken); + var backchannelCompletitionSource = new TaskCompletionSource(); - if (useRichConsole) - { - // We wait for the back channel to be created to signal that - // the AppHost is ready to accept requests. - var backchannel = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":linked_paperclips: Starting Aspire app host...", async context => { - return await backchannelCompletitionSource.Task; - }); - - // We wait for the first update of the console model via RPC from the AppHost. - var dashboardUrls = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":chart_increasing: Starting Aspire dashboard...", async context => { - return await backchannel.GetDashboardUrlsAsync(cancellationToken); - }); - - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:"); - if (dashboardUrls.CodespacesUrlWithLoginToken is not null) - { - AnsiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); - AnsiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]"); - } - else + var pendingRun = _runner.RunAsync( + effectiveAppHostProjectFile, + watch, + !watch, + Array.Empty(), + env, + backchannelCompletitionSource, + cancellationToken); + + if (useRichConsole) { - AnsiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); - } - AnsiConsole.WriteLine(); + // We wait for the back channel to be created to signal that + // the AppHost is ready to accept requests. + var backchannel = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync(":linked_paperclips: Starting Aspire app host...", async context => { + return await backchannelCompletitionSource.Task; + }); + + // We wait for the first update of the console model via RPC from the AppHost. + var dashboardUrls = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync(":chart_increasing: Starting Aspire dashboard...", async context => { + return await backchannel.GetDashboardUrlsAsync(cancellationToken); + }); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:"); + if (dashboardUrls.CodespacesUrlWithLoginToken is not null) + { + AnsiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); + AnsiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]"); + } + else + { + AnsiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); + } + AnsiConsole.WriteLine(); - var table = new Table().Border(TableBorder.Rounded); + var table = new Table().Border(TableBorder.Rounded); - await AnsiConsole.Live(table).StartAsync(async context => { + await AnsiConsole.Live(table).StartAsync(async context => { - var knownResources = new SortedDictionary(); + var knownResources = new SortedDictionary(); - table.AddColumn("Resource"); - table.AddColumn("Type"); - table.AddColumn("State"); - table.AddColumn("Endpoint(s)"); + table.AddColumn("Resource"); + table.AddColumn("Type"); + table.AddColumn("State"); + table.AddColumn("Endpoint(s)"); - var resourceStates = backchannel.GetResourceStatesAsync(cancellationToken); + var resourceStates = backchannel.GetResourceStatesAsync(cancellationToken); - try - { - await foreach(var resourceState in resourceStates) + try { - knownResources[resourceState.Resource] = resourceState; + await foreach(var resourceState in resourceStates) + { + knownResources[resourceState.Resource] = resourceState; - table.Rows.Clear(); + table.Rows.Clear(); - foreach (var knownResource in knownResources) - { - var nameRenderable = new Text(knownResource.Key, new Style().Foreground(Color.White)); - - var typeRenderable = new Text(knownResource.Value.Type, new Style().Foreground(Color.White)); - - var stateRenderable = knownResource.Value.State switch { - "Running" => new Text(knownResource.Value.State, new Style().Foreground(Color.Green)), - "Starting" => new Text(knownResource.Value.State, new Style().Foreground(Color.LightGreen)), - "FailedToStart" => new Text(knownResource.Value.State, new Style().Foreground(Color.Red)), - "Waiting" => new Text(knownResource.Value.State, new Style().Foreground(Color.White)), - "Unhealthy" => new Text(knownResource.Value.State, new Style().Foreground(Color.Yellow)), - "Exited" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), - "Finished" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), - "NotStarted" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), - _ => new Text(knownResource.Value.State ?? "Unknown", new Style().Foreground(Color.Grey)) - }; - - IRenderable endpointsRenderable = new Text("None"); - if (knownResource.Value.Endpoints?.Length > 0) + foreach (var knownResource in knownResources) { - endpointsRenderable = new Rows( - knownResource.Value.Endpoints.Select(e => new Text(e, new Style().Link(e))) - ); + var nameRenderable = new Text(knownResource.Key, new Style().Foreground(Color.White)); + + var typeRenderable = new Text(knownResource.Value.Type, new Style().Foreground(Color.White)); + + var stateRenderable = knownResource.Value.State switch { + "Running" => new Text(knownResource.Value.State, new Style().Foreground(Color.Green)), + "Starting" => new Text(knownResource.Value.State, new Style().Foreground(Color.LightGreen)), + "FailedToStart" => new Text(knownResource.Value.State, new Style().Foreground(Color.Red)), + "Waiting" => new Text(knownResource.Value.State, new Style().Foreground(Color.White)), + "Unhealthy" => new Text(knownResource.Value.State, new Style().Foreground(Color.Yellow)), + "Exited" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), + "Finished" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), + "NotStarted" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), + _ => new Text(knownResource.Value.State ?? "Unknown", new Style().Foreground(Color.Grey)) + }; + + IRenderable endpointsRenderable = new Text("None"); + if (knownResource.Value.Endpoints?.Length > 0) + { + endpointsRenderable = new Rows( + knownResource.Value.Endpoints.Select(e => new Text(e, new Style().Link(e))) + ); + } + + table.AddRow(nameRenderable, typeRenderable, stateRenderable, endpointsRenderable); } - table.AddRow(nameRenderable, typeRenderable, stateRenderable, endpointsRenderable); + context.Refresh(); } - - context.Refresh(); } - } - catch (ConnectionLostException ex) when (ex.InnerException is OperationCanceledException) - { - // This exception will be thrown if the cancellation request reaches the WaitForExitAsync - // call on the process and shuts down the apphost before the JsonRpc connection gets it meaning - // that the apphost side of the RPC connection will be closed. Therefore if we get a - // ConnectionLostException AND the inner exception is an OperationCancelledException we can - // asume that the apphost was shutdown and we can ignore it. - } - catch (OperationCanceledException) - { - // This exception will be thrown if the cancellation request reaches the our side - // of the backchannel side first and the connection is torn down on our-side - // gracefully. We can ignore this exception as well. - } - }); + catch (ConnectionLostException ex) when (ex.InnerException is OperationCanceledException) + { + // This exception will be thrown if the cancellation request reaches the WaitForExitAsync + // call on the process and shuts down the apphost before the JsonRpc connection gets it meaning + // that the apphost side of the RPC connection will be closed. Therefore if we get a + // ConnectionLostException AND the inner exception is an OperationCancelledException we can + // asume that the apphost was shutdown and we can ignore it. + } + catch (OperationCanceledException) + { + // This exception will be thrown if the cancellation request reaches the our side + // of the backchannel side first and the connection is torn down on our-side + // gracefully. We can ignore this exception as well. + } + }); - return await pendingRun; + return await pendingRun; + } + else + { + return await pendingRun; + } } - else + catch (AppHostIncompatibleException ex) { - return await pendingRun; + return InteractionUtils.DisplayIncompatibleVersionError( + ex, + appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + ); } } } \ No newline at end of file diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index 7833fedf452..e8d947d1f12 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -126,7 +126,9 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, if (watch && noBuild) { - throw new InvalidOperationException("Cannot use --watch and --no-build at the same time."); + var ex = new InvalidOperationException("Cannot use --watch and --no-build at the same time."); + backchannelCompletionSource?.SetException(ex); + throw ex; } var watchOrRunCommand = watch ? "watch" : "run"; @@ -468,6 +470,22 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas // We don't want to spam the logs with our early connection attempts. } } + catch (AppHostIncompatibleException ex) + { + logger.LogError( + ex, + "The app host is incompatible with the CLI and must be updated to a version that supports the {RequiredCapability} capability.", + ex.RequiredCapability + ); + + // If the app host is incompatable then there is no point + // trying to reconnect, we should propogate the exception + // up to the code that needs to back channel so it can display + // and error message to the user. + backchannelCompletionSource.SetException(ex); + + throw; + } } while (await timer.WaitForNextTickAsync(cancellationToken)); } diff --git a/src/Aspire.Cli/ExitCodeConstants.cs b/src/Aspire.Cli/ExitCodeConstants.cs index e48a10aa003..dd6226afc7e 100644 --- a/src/Aspire.Cli/ExitCodeConstants.cs +++ b/src/Aspire.Cli/ExitCodeConstants.cs @@ -14,4 +14,5 @@ internal static class ExitCodeConstants public const int FailedToBuildArtifacts = 6; public const int FailedToFindProject = 7; public const int FailedToTrustCertificates = 8; + public const int AppHostIncompatible = 9; } \ No newline at end of file diff --git a/src/Aspire.Cli/Utils/AppHostHelper.cs b/src/Aspire.Cli/Utils/AppHostHelper.cs index c6824f09609..6b75b8d4994 100644 --- a/src/Aspire.Cli/Utils/AppHostHelper.cs +++ b/src/Aspire.Cli/Utils/AppHostHelper.cs @@ -11,39 +11,39 @@ internal static class AppHostHelper { private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(AppHostHelper)); - internal static async Task<(bool IsCompatableAppHost, bool SupportsBackchannel)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + internal static async Task<(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatibilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) { var appHostInformation = await GetAppHostInformationAsync(runner, projectFile, cancellationToken); if (appHostInformation.ExitCode != 0) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be analyzed due to a build error. For more information run with --debug switch.[/]"); - return (false, false); + return (false, false, null); } if (!appHostInformation.IsAspireHost) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project is not an Aspire app host project.[/]"); - return (false, false); + return (false, false, null); } if (!SemVersion.TryParse(appHostInformation.AspireHostingSdkVersion, out var aspireSdkVersion)) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Could not parse Aspire SDK version.[/]"); - return (false, false); + return (false, false, null); } var compatibleRanges = SemVersionRange.Parse("^9.2.0-dev", SemVersionRangeOptions.IncludeAllPrerelease); if (!aspireSdkVersion.Satisfies(compatibleRanges)) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The Aspire SDK version '{appHostInformation.AspireHostingSdkVersion}' is not supported. Please update to the latest version.[/]"); - return (false, false); + return (false, false, appHostInformation.AspireHostingSdkVersion); } else { // NOTE: When we go to support < 9.2.0 app hosts this is where we'll make // a determination as to whether the apphsot supports backchannel or not. - return (true, true); + return (true, true, appHostInformation.AspireHostingSdkVersion); } } diff --git a/src/Aspire.Cli/Utils/PromptUtils.cs b/src/Aspire.Cli/Utils/InteractionUtils.cs similarity index 66% rename from src/Aspire.Cli/Utils/PromptUtils.cs rename to src/Aspire.Cli/Utils/InteractionUtils.cs index 4b00b885e1d..cf517af303e 100644 --- a/src/Aspire.Cli/Utils/PromptUtils.cs +++ b/src/Aspire.Cli/Utils/InteractionUtils.cs @@ -1,11 +1,12 @@ // 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.Backchannel; using Spectre.Console; namespace Aspire.Cli.Utils; -internal static class PromptUtils +internal static class InteractionUtils { public static async Task PromptForStringAsync(string promptText, string? defaultValue = null, Func? validator = null, CancellationToken cancellationToken = default) { @@ -42,4 +43,17 @@ public static async Task PromptForSelectionAsync(string promptText, IEnume return await AnsiConsole.PromptAsync(prompt, cancellationToken); } + + public static int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingSdkVersion) + { + var cliInformationalVersion = VersionHelper.GetDefaultTemplateVersion(); + + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The app host is not compatible. Consider upgrading the app host or Aspire CLI.[/]"); + Console.WriteLine(); + AnsiConsole.MarkupLine($"\t[bold]Aspire Hosting SDK Version[/]: {appHostHostingSdkVersion}"); + AnsiConsole.MarkupLine($"\t[bold]Aspire CLI Version[/]: {cliInformationalVersion}"); + AnsiConsole.MarkupLine($"\t[bold]Required Capability[/]: {ex.RequiredCapability}"); + Console.WriteLine(); + return ExitCodeConstants.AppHostIncompatible; + } } \ No newline at end of file diff --git a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs index 667742b7210..fe50ca47efb 100644 --- a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs +++ b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs @@ -138,4 +138,31 @@ public async Task GetPublishersAsync(CancellationToken cancellationTok var publishers = e.Advertisements.Select(x => x.Name); return [..publishers]; } + +#pragma warning disable CA1822 + public Task GetCapabilitiesAsync(CancellationToken cancellationToken) + { + // The purpose of this API is to allow the CLI to determine what API surfaces + // the AppHost supports. In 9.2 we'll be saying that you need a 9.2 apphost, + // but the 9.3 CLI might actually support working with 9.2 apphosts. The idea + // is that when the backchannel is established the CLI will call this API + // and store the results. The "baseline.v0" capability is the bare minimum + // that we need as of CLI version 9.2-preview*. + // + // Some capabilties will be opt in. For example in 9.3 we might refine the + // publishing activities API to return more information, or add log streaming + // features. So that would add a new capability that the apphsot can report + // on initial backchannel negotiation and the CLI can adapt its behavior around + // that. There may be scenarios where we need to break compataiblity at which + // point we might increase the baseline version that the apphost reports. + // + // The ability to support a back channel at all is determined by the CLI by + // making sure that the apphost version is at least > 9.2. + + _ = cancellationToken; + return Task.FromResult(new string[] { + "baseline.v0" + }); + } +#pragma warning restore CA1822 } \ No newline at end of file From 4c382ccf616c38c62c7a0496d67ad3c3ddccf3f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:38:55 -0700 Subject: [PATCH 20/30] Fix a regression in endpoint resolution in environment variables - Endpoints wrapped in connection strings were throwing during the preproces pass of the evalution. Avoid throwing during that pass - Added tests for this scenario (#8610) Fixes #8596 Co-authored-by: David Fowler --- .../ApplicationModel/ExpressionResolver.cs | 4 ++- .../ExpressionResolverTests.cs | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs index 5764bdf362f..876920347d4 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs @@ -158,7 +158,9 @@ async Task ResolveConnectionStringReferenceAsync(ConnectionString // However, ConnectionStringReference#GetValueAsync will throw if the connection string is not optional but is not present. // so we need to do the same here. var value = await ResolveInternalAsync(cs.Resource.ConnectionStringExpression).ConfigureAwait(false); - if (string.IsNullOrEmpty(value.Value) && !cs.Optional) + + // While pre-processing the endpoints, we never throw + if (!Preprocess && string.IsNullOrEmpty(value.Value) && !cs.Optional) { cs.ThrowConnectionStringUnavailableException(); } diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index 9519e92b88f..2c7c868a732 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -163,6 +163,41 @@ public async Task HostUrlPropertyGetsResolvedInOtlpExporterEndpoint(bool contain var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName").DefaultTimeout(); Assert.Equal(expectedValue, config["OTEL_EXPORTER_OTLP_ENDPOINT"]); } + + [Fact] + public async Task ContainerToContainerEndpointShouldResolve() + { + var builder = DistributedApplication.CreateBuilder(); + + var connectionStringResource = builder.AddResource(new MyContainerResource("myContainer")) + .WithImage("redis") + .WithHttpEndpoint(targetPort: 8080) + .WithEndpoint("http", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 8001, "ContainerHostName", "{{ targetPort }}"); + }); + + var dep = builder.AddContainer("container", "redis") + .WithReference(connectionStringResource) + .WaitFor(connectionStringResource); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dep.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance, "ContainerHostName").DefaultTimeout(); + + Assert.Equal("http://myContainer:8080", config["ConnectionStrings__myContainer"]); + } +} + +sealed class MyContainerResource : ContainerResource, IResourceWithConnectionString +{ + public MyContainerResource(string name) : base(name) + { + PrimaryEndpoint = new(this, "http"); + } + + public EndpointReference PrimaryEndpoint { get; } + + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create($"{PrimaryEndpoint.Property(EndpointProperty.Url)}"); } sealed class TestValueProviderResource(string name) : Resource(name), IValueProvider From 83ce993de4585699c4ac1a7cd5c31260170c6c9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:54:35 -0500 Subject: [PATCH 21/30] [release/9.2] AddAzureContainerAppEnvironment should use the environment name as a prefix (#8607) * AddAzureContainerAppEnvironment should use the environment name as a prefix We should be prefixing the resources created via AddAzureContainerAppEnvironment with the environment name. This will allow for multiple environments to be in a single distributed application in the future. With this change, it means deploying to existing environments will duplicate resources. To solve that, add a new method WithAzdResourceNaming, which will revert the resource names back to the previous naming scheme. * Revert local testing changes * Ensure resourceToken is declared before it is used in bicep * Support volume names * Remove dots and dashes from the volume name to align with azd naming --------- Co-authored-by: Eric Erhardt --- .../infra.module.bicep | 68 +-- .../AzdAzureContainerAppEnvironment.cs | 8 +- .../AzureContainerAppEnvironmentResource.cs | 14 +- .../AzureContainerAppExtensions.cs | 89 +++- .../AzureContainerAppsInfrastructure.cs | 10 +- .../IAzureContainerAppEnvironment.cs | 2 +- .../AzureContainerAppsTests.cs | 443 ++++++++++++------ 7 files changed, 437 insertions(+), 197 deletions(-) diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/infra.module.bicep b/playground/AzureContainerApps/AzureContainerApps.AppHost/infra.module.bicep index cceee108f0b..f3755339344 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/infra.module.bicep +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/infra.module.bicep @@ -5,14 +5,14 @@ param userPrincipalId string param tags object = { } -resource mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: take('mi-${uniqueString(resourceGroup().id)}', 128) +resource infra_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: take('infra_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { - name: take('acr${uniqueString(resourceGroup().id)}', 50) +resource infra_acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: take('infraacr${uniqueString(resourceGroup().id)}', 50) location: location sku: { name: 'Basic' @@ -20,18 +20,18 @@ resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { tags: tags } -resource acr_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(acr.id, mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) +resource infra_acr_infra_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(infra_acr.id, infra_mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) properties: { - principalId: mi.properties.principalId + principalId: infra_mi.properties.principalId roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') principalType: 'ServicePrincipal' } - scope: acr + scope: infra_acr } -resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { - name: take('law-${uniqueString(resourceGroup().id)}', 63) +resource infra_law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: take('infralaw-${uniqueString(resourceGroup().id)}', 63) location: location properties: { sku: { @@ -41,15 +41,15 @@ resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { tags: tags } -resource cae 'Microsoft.App/managedEnvironments@2024-03-01' = { - name: take('cae${uniqueString(resourceGroup().id)}', 24) +resource infra 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: take('infra${uniqueString(resourceGroup().id)}', 24) location: location properties: { appLogsConfiguration: { destination: 'log-analytics' logAnalyticsConfiguration: { - customerId: law.properties.customerId - sharedKey: law.listKeys().primarySharedKey + customerId: infra_law.properties.customerId + sharedKey: infra_law.listKeys().primarySharedKey } } workloadProfiles: [ @@ -67,20 +67,20 @@ resource aspireDashboard 'Microsoft.App/managedEnvironments/dotNetComponents@202 properties: { componentType: 'AspireDashboard' } - parent: cae + parent: infra } -resource cae_Contributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(cae.id, userPrincipalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) +resource infra_Contributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(infra.id, userPrincipalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) properties: { principalId: userPrincipalId roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') } - scope: cae + scope: infra } -resource storageVolume 'Microsoft.Storage/storageAccounts@2024-01-01' = { - name: take('storagevolume${uniqueString(resourceGroup().id)}', 24) +resource infra_storageVolume 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('infrastoragevolume${uniqueString(resourceGroup().id)}', 24) kind: 'StorageV2' location: location sku: { @@ -94,7 +94,7 @@ resource storageVolume 'Microsoft.Storage/storageAccounts@2024-01-01' = { resource storageVolumeFileService 'Microsoft.Storage/storageAccounts/fileServices@2024-01-01' = { name: 'default' - parent: storageVolume + parent: infra_storageVolume } resource shares_volumes_cache_0 'Microsoft.Storage/storageAccounts/fileServices/shares@2024-01-01' = { @@ -110,33 +110,33 @@ resource managedStorage_volumes_cache_0 'Microsoft.App/managedEnvironments/stora name: take('managedstoragevolumescache${uniqueString(resourceGroup().id)}', 24) properties: { azureFile: { - accountName: storageVolume.name - accountKey: storageVolume.listKeys().keys[0].value + accountName: infra_storageVolume.name + accountKey: infra_storageVolume.listKeys().keys[0].value accessMode: 'ReadWrite' shareName: shares_volumes_cache_0.name } } - parent: cae + parent: infra } output volumes_cache_0 string = managedStorage_volumes_cache_0.name -output MANAGED_IDENTITY_NAME string = mi.name +output MANAGED_IDENTITY_NAME string = infra_mi.name -output MANAGED_IDENTITY_PRINCIPAL_ID string = mi.properties.principalId +output MANAGED_IDENTITY_PRINCIPAL_ID string = infra_mi.properties.principalId -output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = law.name +output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = infra_law.name -output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = law.id +output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = infra_law.id -output AZURE_CONTAINER_REGISTRY_NAME string = acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = infra_acr.name -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = acr.properties.loginServer +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = infra_acr.properties.loginServer -output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = mi.id +output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = infra_mi.id -output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = cae.name +output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = infra.name -output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = cae.id +output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = infra.id -output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = cae.properties.defaultDomain \ No newline at end of file +output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = infra.properties.defaultDomain \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzdAzureContainerAppEnvironment.cs b/src/Aspire.Hosting.Azure.AppContainers/AzdAzureContainerAppEnvironment.cs index 86b5001fdb6..23a69108406 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzdAzureContainerAppEnvironment.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzdAzureContainerAppEnvironment.cs @@ -33,9 +33,9 @@ public IManifestExpressionProvider GetSecretOutputKeyVault(AzureBicepResource re return SecretOutputExpression.GetSecretOutputKeyVault(resource); } - public IManifestExpressionProvider GetVolumeStorage(IResource resource, ContainerMountType type, string volumeIndex) + public IManifestExpressionProvider GetVolumeStorage(IResource resource, ContainerMountAnnotation volume, int volumeIndex) { - return VolumeStorageExpression.GetVolumeStorage(resource, type, volumeIndex); + return VolumeStorageExpression.GetVolumeStorage(resource, volume.Type, volumeIndex); } /// @@ -73,7 +73,7 @@ public static IManifestExpressionProvider GetSecretOutputKeyVault(AzureBicepReso /// /// Generates expressions for the volume storage account. That azd creates. /// - private sealed class VolumeStorageExpression(IResource resource, ContainerMountType type, string index) : IManifestExpressionProvider + private sealed class VolumeStorageExpression(IResource resource, ContainerMountType type, int index) : IManifestExpressionProvider { public string ValueExpression => type switch { @@ -82,7 +82,7 @@ private sealed class VolumeStorageExpression(IResource resource, ContainerMountT _ => throw new NotSupportedException() }; - public static IManifestExpressionProvider GetVolumeStorage(IResource resource, ContainerMountType type, string index) => + public static IManifestExpressionProvider GetVolumeStorage(IResource resource, ContainerMountType type, int index) => new VolumeStorageExpression(resource, type, index); } } diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs index 276cce3e9f6..06336ef87ab 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs @@ -13,6 +13,8 @@ namespace Aspire.Hosting.Azure.AppContainers; public class AzureContainerAppEnvironmentResource(string name, Action configureInfrastructure) : AzureProvisioningResource(name, configureInfrastructure), IAzureContainerAppEnvironment { + internal bool UseAzdNamingConvention { get; set; } + /// /// Gets the unique identifier of the Container App Environment. /// @@ -53,7 +55,7 @@ public class AzureContainerAppEnvironmentResource(string name, Action private BicepOutputReference ContainerAppEnvironmentName => new("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", this); - internal Dictionary VolumeNames { get; } = []; + internal Dictionary VolumeNames { get; } = []; IManifestExpressionProvider IAzureContainerAppEnvironment.ContainerAppEnvironmentId => ContainerAppEnvironmentId; @@ -76,18 +78,18 @@ IManifestExpressionProvider IAzureContainerAppEnvironment.GetSecretOutputKeyVaul throw new NotSupportedException("Automatic Key vault generation is not supported in this environment. Please create a key vault resource directly."); } - IManifestExpressionProvider IAzureContainerAppEnvironment.GetVolumeStorage(IResource resource, ContainerMountType type, string volumeIndex) + IManifestExpressionProvider IAzureContainerAppEnvironment.GetVolumeStorage(IResource resource, ContainerMountAnnotation volume, int volumeIndex) { // REVIEW: Should we use the same naming algorithm as azd? var outputName = $"volumes_{resource.Name}_{volumeIndex}"; - if (!VolumeNames.TryGetValue(outputName, out var outputReference)) + if (!VolumeNames.TryGetValue(outputName, out var volumeName)) { - outputReference = new BicepOutputReference(outputName, this); + volumeName = (resource, volume, volumeIndex, new BicepOutputReference(outputName, this)); - VolumeNames[outputName] = outputReference; + VolumeNames[outputName] = volumeName; } - return outputReference; + return volumeName.outputReference; } } diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs index fdee3e1a583..639462463b2 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Aspire.Hosting.Azure.AppContainers; @@ -64,6 +65,7 @@ public static IResourceBuilder AddAzureCon var containerAppEnvResource = new AzureContainerAppEnvironmentResource(name, static infra => { + var appEnvResource = (AzureContainerAppEnvironmentResource)infra.AspireResource; var userPrincipalId = new ProvisioningParameter("userPrincipalId", typeof(string)); infra.Add(userPrincipalId); @@ -75,14 +77,24 @@ public static IResourceBuilder AddAzureCon infra.Add(tags); - var identity = new UserAssignedIdentity("mi") + ProvisioningVariable? resourceToken = null; + if (appEnvResource.UseAzdNamingConvention) + { + resourceToken = new ProvisioningVariable("resourceToken", typeof(string)) + { + Value = BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id) + }; + infra.Add(resourceToken); + } + + var identity = new UserAssignedIdentity(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_mi")) { Tags = tags }; infra.Add(identity); - var containerRegistry = new ContainerRegistryService("acr") + var containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_acr")) { Sku = new() { Name = ContainerRegistrySkuName.Basic }, Tags = tags @@ -96,7 +108,7 @@ public static IResourceBuilder AddAzureCon pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, identity.Id, pullRa.RoleDefinitionId); infra.Add(pullRa); - var laWorkspace = new OperationalInsightsWorkspace("law") + var laWorkspace = new OperationalInsightsWorkspace(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_law")) { Sku = new() { Name = OperationalInsightsWorkspaceSkuName.PerGB2018 }, Tags = tags @@ -104,7 +116,7 @@ public static IResourceBuilder AddAzureCon infra.Add(laWorkspace); - var containerAppEnvironment = new ContainerAppManagedEnvironment("cae") + var containerAppEnvironment = new ContainerAppManagedEnvironment(appEnvResource.GetBicepIdentifier()) { WorkloadProfiles = [ new ContainerAppWorkloadProfile() @@ -149,9 +161,10 @@ public static IResourceBuilder AddAzureCon var resource = (AzureContainerAppEnvironmentResource)infra.AspireResource; + StorageAccount? storageVolume = null; if (resource.VolumeNames.Count > 0) { - var storageVolume = new StorageAccount("storageVolume") + storageVolume = new StorageAccount(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_storageVolume")) { Tags = tags, Sku = new StorageSku() { Name = StorageSkuName.StandardLrs }, @@ -200,6 +213,29 @@ public static IResourceBuilder AddAzureCon infra.Add(containerAppStorage); managedStorages[outputName] = containerAppStorage; + + if (appEnvResource.UseAzdNamingConvention) + { + var volumeName = output.volume.Type switch + { + ContainerMountType.BindMount => $"bm{output.index}", + ContainerMountType.Volume => output.volume.Source ?? $"v{output.index}", + _ => throw new NotSupportedException() + }; + + // Remove '.' and '-' characters from volumeName + volumeName = volumeName.Replace(".", "").Replace("-", ""); + + share.Name = BicepFunction.Take( + BicepFunction.Interpolate( + $"{BicepFunction.ToLower(output.resource.Name)}-{BicepFunction.ToLower(volumeName)}"), + 60); + + containerAppStorage.Name = BicepFunction.Take( + BicepFunction.Interpolate( + $"{BicepFunction.ToLower(output.resource.Name)}-{BicepFunction.ToLower(volumeName)}"), + 32); + } } } @@ -208,10 +244,34 @@ public static IResourceBuilder AddAzureCon { infra.Add(new ProvisioningOutput(key, typeof(string)) { - Value = value.Name + // use an expression here in case the resource's Name was set to a function expression above + Value = new MemberExpression(new IdentifierExpression(value.BicepIdentifier), "name") }); } + if (appEnvResource.UseAzdNamingConvention) + { + Debug.Assert(resourceToken is not null); + + identity.Name = BicepFunction.Interpolate($"mi-{resourceToken}"); + containerRegistry.Name = new FunctionCallExpression( + new IdentifierExpression("replace"), + new InterpolatedStringExpression( + [ + new StringLiteralExpression("acr-"), + new IdentifierExpression(resourceToken.BicepIdentifier) + ]), + new StringLiteralExpression("-"), + new StringLiteralExpression("")); + laWorkspace.Name = BicepFunction.Interpolate($"law-{resourceToken}"); + containerAppEnvironment.Name = BicepFunction.Interpolate($"cae-{resourceToken}"); + + if (storageVolume is not null) + { + storageVolume.Name = BicepFunction.Interpolate($"vol{resourceToken}"); + } + } + infra.Add(new ProvisioningOutput("MANAGED_IDENTITY_NAME", typeof(string)) { Value = identity.Name @@ -272,4 +332,21 @@ public static IResourceBuilder AddAzureCon return builder.AddResource(containerAppEnvResource); } + + /// + /// Configures the container app environment resources to use the same naming conventions as azd. + /// + /// The AzureContainerAppEnvironmentResource to configure. + /// + /// + /// By default, the container app environment resources use a different naming convention than azd. + /// + /// This method allows for reusing the previously deployed resources if the application was deployed using + /// azd without calling + /// + public static IResourceBuilder WithAzdResourceNaming(this IResourceBuilder builder) + { + builder.Resource.UseAzdNamingConvention = true; + return builder; + } } diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs index cfce5b42e64..6e50d12c716 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs @@ -600,8 +600,8 @@ private void ProcessVolumes() { var (index, volumeName) = volume.Type switch { - ContainerMountType.BindMount => ($"{bindMountIndex}", $"bm{bindMountIndex}"), - ContainerMountType.Volume => ($"{volumeIndex}", $"v{volumeIndex}"), + ContainerMountType.BindMount => (bindMountIndex, $"bm{bindMountIndex}"), + ContainerMountType.Volume => (volumeIndex, $"v{volumeIndex}"), _ => throw new NotSupportedException() }; @@ -614,7 +614,7 @@ private void ProcessVolumes() volumeIndex++; } - var volumeStorageParameter = AllocateVolumeStorageAccount(volume.Type, index); + var volumeStorageParameter = AllocateVolumeStorageAccount(volume, index); var containerAppVolume = new ContainerAppVolume { @@ -768,8 +768,8 @@ BicepValue GetHostValue(string? prefix = null, string? suffix = null) throw new NotSupportedException("Unsupported value type " + value.GetType()); } - private ProvisioningParameter AllocateVolumeStorageAccount(ContainerMountType type, string volumeIndex) => - AllocateParameter(_containerAppEnvironmentContext.Environment.GetVolumeStorage(resource, type, volumeIndex)); + private ProvisioningParameter AllocateVolumeStorageAccount(ContainerMountAnnotation volume, int volumeIndex) => + AllocateParameter(_containerAppEnvironmentContext.Environment.GetVolumeStorage(resource, volume, volumeIndex)); private BicepValue AllocateKeyVaultSecretUriReference(BicepSecretOutputReference secretOutputReference) { diff --git a/src/Aspire.Hosting.Azure.AppContainers/IAzureContainerAppEnvironment.cs b/src/Aspire.Hosting.Azure.AppContainers/IAzureContainerAppEnvironment.cs index 9a70e8faea9..b2d6d63778c 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/IAzureContainerAppEnvironment.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/IAzureContainerAppEnvironment.cs @@ -17,5 +17,5 @@ internal interface IAzureContainerAppEnvironment IManifestExpressionProvider ContainerAppEnvironmentName { get; } IManifestExpressionProvider GetSecretOutputKeyVault(AzureBicepResource resource); - IManifestExpressionProvider GetVolumeStorage(IResource resource, ContainerMountType type, string volumeIndex); + IManifestExpressionProvider GetVolumeStorage(IResource resource, ContainerMountAnnotation volume, int volumeIndex); } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs index 8515984717c..4fb643c8fd1 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs @@ -3212,12 +3212,19 @@ public async Task KnownParametersAreNotSetWhenUsingAzdResources() } } - [Fact] - public async Task AddContainerAppEnvironmentAddsEnvironmentResource() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task AddContainerAppEnvironmentAddsEnvironmentResource(bool useAzdNaming) { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppEnvironment("env"); + var env = builder.AddAzureContainerAppEnvironment("env"); + + if (useAzdNaming) + { + env.WithAzdResourceNaming(); + } var pg = builder.AddAzurePostgresFlexibleServer("pg") .WithPasswordAuthentication() @@ -3252,151 +3259,305 @@ public async Task AddContainerAppEnvironmentAddsEnvironmentResource() Assert.Equal(expectedManifest, m); - var expectedBicep = - """ - @description('The location for the resource(s) to be deployed.') - param location string = resourceGroup().location - - param userPrincipalId string - - param tags object = { } - - resource mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: take('mi-${uniqueString(resourceGroup().id)}', 128) - location: location - tags: tags - } - - resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { - name: take('acr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags - } - - resource acr_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(acr.id, mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) - properties: { - principalId: mi.properties.principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - principalType: 'ServicePrincipal' - } - scope: acr - } - - resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { - name: take('law-${uniqueString(resourceGroup().id)}', 63) - location: location - properties: { - sku: { - name: 'PerGB2018' + string expectedBicep; + if (useAzdNaming) + { + expectedBicep = + """ + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param userPrincipalId string + + param tags object = { } + + var resourceToken = uniqueString(resourceGroup().id) + + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: 'mi-${resourceToken}' + location: location + tags: tags } - } - tags: tags - } - - resource cae 'Microsoft.App/managedEnvironments@2024-03-01' = { - name: take('cae${uniqueString(resourceGroup().id)}', 24) - location: location - properties: { - appLogsConfiguration: { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: law.properties.customerId - sharedKey: law.listKeys().primarySharedKey + + resource env_acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: replace('acr-${resourceToken}', '-', '') + location: location + sku: { + name: 'Basic' } + tags: tags } - workloadProfiles: [ - { - name: 'consumption' - workloadProfileType: 'Consumption' + + resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(env_acr.id, env_mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) + properties: { + principalId: env_mi.properties.principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + principalType: 'ServicePrincipal' } - ] - } - tags: tags - } - - resource aspireDashboard 'Microsoft.App/managedEnvironments/dotNetComponents@2024-10-02-preview' = { - name: 'aspire-dashboard' - properties: { - componentType: 'AspireDashboard' - } - parent: cae - } - - resource cae_Contributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(cae.id, userPrincipalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) - properties: { - principalId: userPrincipalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') - } - scope: cae - } - - resource storageVolume 'Microsoft.Storage/storageAccounts@2024-01-01' = { - name: take('storagevolume${uniqueString(resourceGroup().id)}', 24) - kind: 'StorageV2' - location: location - sku: { - name: 'Standard_LRS' - } - properties: { - largeFileSharesState: 'Enabled' - } - tags: tags - } - - resource storageVolumeFileService 'Microsoft.Storage/storageAccounts/fileServices@2024-01-01' = { - name: 'default' - parent: storageVolume - } - - resource shares_volumes_cache_0 'Microsoft.Storage/storageAccounts/fileServices/shares@2024-01-01' = { - name: take('sharesvolumescache0-${uniqueString(resourceGroup().id)}', 63) - properties: { - enabledProtocols: 'SMB' - shareQuota: 1024 - } - parent: storageVolumeFileService + scope: env_acr + } + + resource env_law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: 'law-${resourceToken}' + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } + tags: tags + } + + resource env 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: 'cae-${resourceToken}' + location: location + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: env_law.properties.customerId + sharedKey: env_law.listKeys().primarySharedKey + } + } + workloadProfiles: [ + { + name: 'consumption' + workloadProfileType: 'Consumption' + } + ] + } + tags: tags + } + + resource aspireDashboard 'Microsoft.App/managedEnvironments/dotNetComponents@2024-10-02-preview' = { + name: 'aspire-dashboard' + properties: { + componentType: 'AspireDashboard' + } + parent: env + } + + resource env_Contributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(env.id, userPrincipalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) + properties: { + principalId: userPrincipalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + } + scope: env + } + + resource env_storageVolume 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: 'vol${resourceToken}' + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_LRS' + } + properties: { + largeFileSharesState: 'Enabled' + } + tags: tags + } + + resource storageVolumeFileService 'Microsoft.Storage/storageAccounts/fileServices@2024-01-01' = { + name: 'default' + parent: env_storageVolume + } + + resource shares_volumes_cache_0 'Microsoft.Storage/storageAccounts/fileServices/shares@2024-01-01' = { + name: take('${toLower('cache')}-${toLower('data')}', 60) + properties: { + enabledProtocols: 'SMB' + shareQuota: 1024 + } + parent: storageVolumeFileService + } + + resource managedStorage_volumes_cache_0 'Microsoft.App/managedEnvironments/storages@2024-03-01' = { + name: take('${toLower('cache')}-${toLower('data')}', 32) + properties: { + azureFile: { + accountName: env_storageVolume.name + accountKey: env_storageVolume.listKeys().keys[0].value + accessMode: 'ReadWrite' + shareName: shares_volumes_cache_0.name + } + } + parent: env + } + + output volumes_cache_0 string = managedStorage_volumes_cache_0.name + + output MANAGED_IDENTITY_NAME string = 'mi-${resourceToken}' + + output MANAGED_IDENTITY_PRINCIPAL_ID string = env_mi.properties.principalId + + output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = 'law-${resourceToken}' + + output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = env_law.id + + output AZURE_CONTAINER_REGISTRY_NAME string = replace('acr-${resourceToken}', '-', '') + + output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer + + output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = env_mi.id + + output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = 'cae-${resourceToken}' + + output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = env.id + + output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = env.properties.defaultDomain + """; } - - resource managedStorage_volumes_cache_0 'Microsoft.App/managedEnvironments/storages@2024-03-01' = { - name: take('managedstoragevolumescache${uniqueString(resourceGroup().id)}', 24) - properties: { - azureFile: { - accountName: storageVolume.name - accountKey: storageVolume.listKeys().keys[0].value - accessMode: 'ReadWrite' - shareName: shares_volumes_cache_0.name + else + { + expectedBicep = + """ + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param userPrincipalId string + + param tags object = { } + + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) + location: location + tags: tags } - } - parent: cae + + resource env_acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: take('envacr${uniqueString(resourceGroup().id)}', 50) + location: location + sku: { + name: 'Basic' + } + tags: tags + } + + resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(env_acr.id, env_mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) + properties: { + principalId: env_mi.properties.principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + principalType: 'ServicePrincipal' + } + scope: env_acr + } + + resource env_law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: take('envlaw-${uniqueString(resourceGroup().id)}', 63) + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } + tags: tags + } + + resource env 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: take('env${uniqueString(resourceGroup().id)}', 24) + location: location + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: env_law.properties.customerId + sharedKey: env_law.listKeys().primarySharedKey + } + } + workloadProfiles: [ + { + name: 'consumption' + workloadProfileType: 'Consumption' + } + ] + } + tags: tags + } + + resource aspireDashboard 'Microsoft.App/managedEnvironments/dotNetComponents@2024-10-02-preview' = { + name: 'aspire-dashboard' + properties: { + componentType: 'AspireDashboard' + } + parent: env + } + + resource env_Contributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(env.id, userPrincipalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) + properties: { + principalId: userPrincipalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + } + scope: env + } + + resource env_storageVolume 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('envstoragevolume${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_LRS' + } + properties: { + largeFileSharesState: 'Enabled' + } + tags: tags + } + + resource storageVolumeFileService 'Microsoft.Storage/storageAccounts/fileServices@2024-01-01' = { + name: 'default' + parent: env_storageVolume + } + + resource shares_volumes_cache_0 'Microsoft.Storage/storageAccounts/fileServices/shares@2024-01-01' = { + name: take('sharesvolumescache0-${uniqueString(resourceGroup().id)}', 63) + properties: { + enabledProtocols: 'SMB' + shareQuota: 1024 + } + parent: storageVolumeFileService + } + + resource managedStorage_volumes_cache_0 'Microsoft.App/managedEnvironments/storages@2024-03-01' = { + name: take('managedstoragevolumescache${uniqueString(resourceGroup().id)}', 24) + properties: { + azureFile: { + accountName: env_storageVolume.name + accountKey: env_storageVolume.listKeys().keys[0].value + accessMode: 'ReadWrite' + shareName: shares_volumes_cache_0.name + } + } + parent: env + } + + output volumes_cache_0 string = managedStorage_volumes_cache_0.name + + output MANAGED_IDENTITY_NAME string = env_mi.name + + output MANAGED_IDENTITY_PRINCIPAL_ID string = env_mi.properties.principalId + + output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = env_law.name + + output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = env_law.id + + output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name + + output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer + + output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = env_mi.id + + output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = env.name + + output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = env.id + + output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = env.properties.defaultDomain + """; } - - output volumes_cache_0 string = managedStorage_volumes_cache_0.name - - output MANAGED_IDENTITY_NAME string = mi.name - - output MANAGED_IDENTITY_PRINCIPAL_ID string = mi.properties.principalId - - output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = law.name - - output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = law.id - - output AZURE_CONTAINER_REGISTRY_NAME string = acr.name - - output AZURE_CONTAINER_REGISTRY_ENDPOINT string = acr.properties.loginServer - - output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = mi.id - - output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = cae.name - - output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = cae.id - - output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = cae.properties.defaultDomain - """; output.WriteLine(bicep); Assert.Equal(expectedBicep, bicep); } From 2c4e115f67abf9acb3083cc171a42893f82279e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:16:27 -0700 Subject: [PATCH 22/30] Disable OTEL telemetry in CLI if not a debug build. (#8613) Co-authored-by: Mitch Denny --- src/Aspire.Cli/Program.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index 9d86c0ee70a..3a7cb7e944f 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -9,9 +9,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + +#if DEBUG using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; +#endif + using RootCommand = Aspire.Cli.Commands.RootCommand; namespace Aspire.Cli; @@ -32,6 +36,7 @@ private static IHost BuildApplication(string[] args) logging.IncludeScopes = true; }); +#if DEBUG var otelBuilder = builder.Services .AddOpenTelemetry() .WithTracing(tracing => { @@ -56,6 +61,7 @@ private static IHost BuildApplication(string[] args) // has to finish sending telemetry. otelBuilder.UseOtlpExporter(); } +#endif var debugMode = args?.Any(a => a == "--debug" || a == "-d") ?? false; From 7812d3a2e95d646119d3547a47bae8a810989b27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:54:04 +0000 Subject: [PATCH 23/30] [release/9.2] Filter out tracing health endpoints in service defaults templates (#8644) * Filter out tracing health endpoints Fixes #8580 * Fix typo --------- Co-authored-by: Damian Edwards --- playground/TestShop/TestShop.AppHost/Program.cs | 6 ++++-- .../TestShop.ServiceDefaults/Extensions.cs | 14 +++++++++++--- .../Extensions.cs | 14 +++++++++++--- .../aspire-servicedefaults/9.2/Extensions.cs | 14 +++++++++++--- .../Extensions.cs | 14 +++++++++++--- 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index 401f728815b..9ea61680a14 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -62,14 +62,16 @@ .WithReference(basketCache) .WithReference(messaging).WaitFor(messaging); -builder.AddProject("frontend") +var frontend = builder.AddProject("frontend") .WithExternalHttpEndpoints() .WithReference(basketService) .WithReference(catalogService) .WithUrls(c => c.Urls.ForEach(u => u.DisplayText = $"Online store ({u.Endpoint?.EndpointName})")); +var _ = frontend.GetEndpoint("https").Exists ? frontend.WithHttpsHealthCheck("/health") : frontend.WithHttpHealthCheck("/health"); + builder.AddProject("orderprocessor", launchProfileName: "OrderProcessor") - .WithReference(messaging).WaitFor(messaging); + .WithReference(messaging).WaitFor(messaging); builder.AddProject("apigateway") .WithReference(basketService) diff --git a/playground/TestShop/TestShop.ServiceDefaults/Extensions.cs b/playground/TestShop/TestShop.ServiceDefaults/Extensions.cs index 5001ccebeac..5ec75c84763 100644 --- a/playground/TestShop/TestShop.ServiceDefaults/Extensions.cs +++ b/playground/TestShop/TestShop.ServiceDefaults/Extensions.cs @@ -14,6 +14,9 @@ namespace Microsoft.Extensions.Hosting; // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -52,7 +55,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w }) .WithTracing(tracing => { - tracing.AddAspNetCoreInstrumentation() + tracing + .AddAspNetCoreInstrumentation(tracing => + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -97,10 +105,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); diff --git a/src/Aspire.ProjectTemplates/templates/aspire-empty/9.2/AspireApplication.1.ServiceDefaults/Extensions.cs b/src/Aspire.ProjectTemplates/templates/aspire-empty/9.2/AspireApplication.1.ServiceDefaults/Extensions.cs index 13151bf46d9..112c1281479 100644 --- a/src/Aspire.ProjectTemplates/templates/aspire-empty/9.2/AspireApplication.1.ServiceDefaults/Extensions.cs +++ b/src/Aspire.ProjectTemplates/templates/aspire-empty/9.2/AspireApplication.1.ServiceDefaults/Extensions.cs @@ -15,6 +15,9 @@ namespace Microsoft.Extensions.Hosting; // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -59,7 +62,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); @@ -105,10 +113,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); diff --git a/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/9.2/Extensions.cs b/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/9.2/Extensions.cs index 13151bf46d9..112c1281479 100644 --- a/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/9.2/Extensions.cs +++ b/src/Aspire.ProjectTemplates/templates/aspire-servicedefaults/9.2/Extensions.cs @@ -15,6 +15,9 @@ namespace Microsoft.Extensions.Hosting; // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -59,7 +62,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); @@ -105,10 +113,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/9.2/Aspire-StarterApplication.1.ServiceDefaults/Extensions.cs b/src/Aspire.ProjectTemplates/templates/aspire-starter/9.2/Aspire-StarterApplication.1.ServiceDefaults/Extensions.cs index 13151bf46d9..112c1281479 100644 --- a/src/Aspire.ProjectTemplates/templates/aspire-starter/9.2/Aspire-StarterApplication.1.ServiceDefaults/Extensions.cs +++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/9.2/Aspire-StarterApplication.1.ServiceDefaults/Extensions.cs @@ -15,6 +15,9 @@ namespace Microsoft.Extensions.Hosting; // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); @@ -59,7 +62,12 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); @@ -105,10 +113,10 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) if (app.Environment.IsDevelopment()) { // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + app.MapHealthChecks(HealthEndpointPath); // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); From b89a53956f198cf1118ea00db2c7fe476ca6100a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 8 Apr 2025 15:36:47 -0500 Subject: [PATCH 24/30] Obsolete AddAzureContainerAppsInfrastructure (#8639) (#8642) * Obsolete AddAzureContainerAppsInfrastructure This method is no longer meant to be used. Instead developers should be calling AddAzureContainerAppEnvironment. Covert the tests using this API to the new API * Fix volume output naming issue We weren't discriminating between volumes and bindmounts, which caused a cache collision. * Switch the prefix at the beginning --- .../AzureContainerAppContainerExtensions.cs | 2 +- .../AzureContainerAppEnvironmentResource.cs | 9 +- .../AzureContainerAppExecutableExtensions.cs | 2 +- .../AzureContainerAppExtensions.cs | 8 +- .../AzureContainerAppProjectExtensions.cs | 2 +- .../AzureResourcePreparer.cs | 2 +- .../AzureContainerAppsTests.cs | 434 +++++++++--------- .../AzureCosmosDBExtensionsTests.cs | 2 +- .../AzureFunctionsTests.cs | 8 +- .../AzurePostgresExtensionsTests.cs | 2 +- .../AzureRedisExtensionsTests.cs | 2 +- .../AzureResourcePreparerTests.cs | 6 +- .../AzureSqlExtensionsTests.cs | 2 +- .../AppContainersPublicApiTests.cs | 2 + .../RoleAssignmentTests.cs | 2 +- 15 files changed, 252 insertions(+), 233 deletions(-) diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppContainerExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppContainerExtensions.cs index d498c924883..4c64fd9e7c0 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppContainerExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppContainerExtensions.cs @@ -42,7 +42,7 @@ public static IResourceBuilder PublishAsAzureContainerApp(this IResourceBu return container; } - container.ApplicationBuilder.AddAzureContainerAppsInfrastructure(); + container.ApplicationBuilder.AddAzureContainerAppsInfrastructureCore(); container.WithAnnotation(new AzureContainerAppCustomizationAnnotation(configure)); diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs index 06336ef87ab..bbb42ba3778 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs @@ -80,8 +80,15 @@ IManifestExpressionProvider IAzureContainerAppEnvironment.GetSecretOutputKeyVaul IManifestExpressionProvider IAzureContainerAppEnvironment.GetVolumeStorage(IResource resource, ContainerMountAnnotation volume, int volumeIndex) { + var prefix = volume.Type switch + { + ContainerMountType.BindMount => "bindmounts", + ContainerMountType.Volume => "volumes", + _ => throw new NotSupportedException() + }; + // REVIEW: Should we use the same naming algorithm as azd? - var outputName = $"volumes_{resource.Name}_{volumeIndex}"; + var outputName = $"{prefix}_{resource.Name}_{volumeIndex}"; if (!VolumeNames.TryGetValue(outputName, out var volumeName)) { diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExecutableExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExecutableExtensions.cs index d63d5656fb6..f765d76d809 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExecutableExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExecutableExtensions.cs @@ -42,7 +42,7 @@ public static IResourceBuilder PublishAsAzureContainerApp(this IResourceBu return executable; } - executable.ApplicationBuilder.AddAzureContainerAppsInfrastructure(); + executable.ApplicationBuilder.AddAzureContainerAppsInfrastructureCore(); return executable.WithAnnotation(new AzureContainerAppCustomizationAnnotation(configure)); } diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs index 639462463b2..0e8d87e599f 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs @@ -28,7 +28,11 @@ public static class AzureContainerAppExtensions /// Adds the necessary infrastructure for Azure Container Apps to the distributed application builder. /// /// The distributed application builder. - public static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructure(this IDistributedApplicationBuilder builder) + [Obsolete($"Use {nameof(AddAzureContainerAppEnvironment)} instead. This method will be removed in a future version.")] + public static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructure(this IDistributedApplicationBuilder builder) => + AddAzureContainerAppsInfrastructureCore(builder); + + internal static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructureCore(this IDistributedApplicationBuilder builder) { ArgumentNullException.ThrowIfNull(builder); @@ -52,7 +56,7 @@ public static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructure /// public static IResourceBuilder AddAzureContainerAppEnvironment(this IDistributedApplicationBuilder builder, string name) { - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppsInfrastructureCore(); // Only support one temporarily until we can support multiple environments // and allowing each container app to be explicit about which environment it uses diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppProjectExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppProjectExtensions.cs index 2afb2e4002b..905535b0840 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppProjectExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppProjectExtensions.cs @@ -42,7 +42,7 @@ public static IResourceBuilder PublishAsAzureContainerApp(this IResourceBu return project; } - project.ApplicationBuilder.AddAzureContainerAppsInfrastructure(); + project.ApplicationBuilder.AddAzureContainerAppsInfrastructureCore(); project.WithAnnotation(new AzureContainerAppCustomizationAnnotation(configure)); diff --git a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs index a6d47daccd7..ee405ac0cfa 100644 --- a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs +++ b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs @@ -85,7 +85,7 @@ private static void EnsureNoRoleAssignmentAnnotations(DistributedApplicationMode { if (resource.HasAnnotationOfType()) { - throw new InvalidOperationException("The application model does not support role assignments. Ensure you are using a publisher that supports role assignments, for example AddAzureContainerAppsInfrastructure."); + throw new InvalidOperationException("The application model does not support role assignments. Ensure you are using an environment that supports role assignments, for example AddAzureContainerAppEnvironment."); } } } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs index 4fb643c8fd1..fbfcc3f97c6 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs @@ -25,7 +25,9 @@ public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContaine { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); +#pragma warning disable CS0618 // Type or member is obsolete builder.AddAzureContainerAppsInfrastructure(); +#pragma warning restore CS0618 // Type or member is obsolete builder.AddContainer("api", "myimage"); @@ -101,7 +103,7 @@ public async Task AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithCon { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var directory = Directory.CreateTempSubdirectory(".aspire-test"); @@ -134,10 +136,10 @@ public async Task AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithCon "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -150,13 +152,13 @@ public async Task AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithCon @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param env_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param env_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -168,12 +170,12 @@ param api_containerimage string activeRevisionsMode: 'Single' registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -189,7 +191,7 @@ param api_containerimage string identity: { type: 'UserAssigned' userAssignedIdentities: { - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -203,7 +205,7 @@ public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContaine { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddProject("api", launchProfileName: null) .WithHttpEndpoint(); @@ -233,10 +235,10 @@ public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContaine "path": "api.module.bicep", "params": { "api_containerport": "{api.containerPort}", - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -248,19 +250,19 @@ public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContaine """ @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - + param api_containerport string - param outputs_azure_container_apps_environment_default_domain string - - param outputs_azure_container_apps_environment_id string - - param outputs_azure_container_registry_endpoint string - - param outputs_azure_container_registry_managed_identity_id string - + param env_outputs_azure_container_apps_environment_default_domain string + + param env_outputs_azure_container_apps_environment_id string + + param env_outputs_azure_container_registry_endpoint string + + param env_outputs_azure_container_registry_managed_identity_id string + param api_containerimage string - + resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'api' location: location @@ -274,12 +276,12 @@ param api_containerimage string } registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -317,7 +319,7 @@ param api_containerimage string identity: { type: 'UserAssigned' userAssignedIdentities: { - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -331,7 +333,7 @@ public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastruc { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("infra"); var env = builder.AddParameter("env"); @@ -370,10 +372,10 @@ public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastruc "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "infra_outputs_azure_container_apps_environment_default_domain": "{infra.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "infra_outputs_azure_container_apps_environment_id": "{infra.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "infra_outputs_azure_container_registry_endpoint": "{infra.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "infra_outputs_azure_container_registry_managed_identity_id": "{infra.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}", "env": "{env.value}" } @@ -387,13 +389,13 @@ public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastruc @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param infra_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param infra_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param infra_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param infra_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -407,12 +409,12 @@ param env string activeRevisionsMode: 'Single' registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: infra_outputs_azure_container_registry_endpoint + identity: infra_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: infra_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -434,7 +436,7 @@ param env string identity: { type: 'UserAssigned' userAssignedIdentities: { - '${outputs_azure_container_registry_managed_identity_id}': { } + '${infra_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -448,7 +450,7 @@ public async Task CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExe { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddExecutable("api", "node.exe", Environment.CurrentDirectory) .PublishAsDockerFile(); @@ -477,10 +479,10 @@ public async Task CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExe "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -493,13 +495,13 @@ public async Task CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExe @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param env_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param env_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -511,12 +513,12 @@ param api_containerimage string activeRevisionsMode: 'Single' registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -532,7 +534,7 @@ param api_containerimage string identity: { type: 'UserAssigned' userAssignedIdentities: { - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -546,7 +548,7 @@ public async Task AddContainerAppsInfrastructureWithParameterReference() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var value = builder.AddParameter("value"); var minReplicas = builder.AddParameter("minReplicas"); @@ -587,8 +589,8 @@ public async Task AddContainerAppsInfrastructureWithParameterReference() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", "value": "{value.value}", "minReplicas": "{minReplicas.value}" } @@ -602,9 +604,9 @@ public async Task AddContainerAppsInfrastructureWithParameterReference() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string param value string @@ -617,7 +619,7 @@ param minReplicas string configuration: { activeRevisionsMode: 'Single' } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -648,7 +650,7 @@ public async Task AddContainerAppsEntrypointAndArgs() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithEntrypoint("/bin/sh") @@ -674,9 +676,9 @@ public async Task AddContainerAppsEntrypointAndArgs() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'api' @@ -685,7 +687,7 @@ param outputs_azure_container_apps_environment_id string configuration: { activeRevisionsMode: 'Single' } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -717,7 +719,7 @@ public async Task ProjectWithManyReferenceTypes() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var db = builder.AddAzureCosmosDB("mydb"); db.AddCosmosDatabase("cosmosdb", databaseName: "db"); @@ -800,10 +802,10 @@ public async Task ProjectWithManyReferenceTypes() "value0_value": "{value0.value}", "value1_value": "{value1.value}", "cs_connectionstring": "{cs.connectionString}", - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -846,13 +848,13 @@ param value1_value string @secure() param cs_connectionstring string - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param env_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param env_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -903,12 +905,12 @@ param api_containerimage string } registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -969,11 +971,11 @@ param api_containerimage string } { name: 'HTTP_EP' - value: 'http://api.internal.${outputs_azure_container_apps_environment_default_domain}' + value: 'http://api.internal.${env_outputs_azure_container_apps_environment_default_domain}' } { name: 'HTTPS_EP' - value: 'https://api.internal.${outputs_azure_container_apps_environment_default_domain}' + value: 'https://api.internal.${env_outputs_azure_container_apps_environment_default_domain}' } { name: 'INTERNAL_EP' @@ -989,11 +991,11 @@ param api_containerimage string } { name: 'HOST' - value: 'api.internal.${outputs_azure_container_apps_environment_default_domain}' + value: 'api.internal.${env_outputs_azure_container_apps_environment_default_domain}' } { name: 'HOSTANDPORT' - value: 'api.internal.${outputs_azure_container_apps_environment_default_domain}:80' + value: 'api.internal.${env_outputs_azure_container_apps_environment_default_domain}:80' } { name: 'SCHEME' @@ -1019,7 +1021,7 @@ param api_containerimage string type: 'UserAssigned' userAssignedIdentities: { '${api_identity_outputs_id}': { } - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -1192,7 +1194,7 @@ public async Task PublishAsContainerAppInfluencesContainerAppDefinition() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .PublishAsAzureContainerApp((module, c) => { @@ -1225,8 +1227,8 @@ public async Task PublishAsContainerAppInfluencesContainerAppDefinition() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" } } """; @@ -1238,9 +1240,9 @@ public async Task PublishAsContainerAppInfluencesContainerAppDefinition() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'api' @@ -1249,7 +1251,7 @@ param outputs_azure_container_apps_environment_id string configuration: { activeRevisionsMode: 'Single' } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -1276,7 +1278,7 @@ public async Task ConfigureCustomDomainMutatesIngress() var customDomain = builder.AddParameter("customDomain"); var certificateName = builder.AddParameter("certificateName"); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint(targetPort: 1111) .PublishAsAzureContainerApp((module, c) => @@ -1308,8 +1310,8 @@ public async Task ConfigureCustomDomainMutatesIngress() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", "certificateName": "{certificateName.value}", "customDomain": "{customDomain.value}" } @@ -1323,9 +1325,9 @@ public async Task ConfigureCustomDomainMutatesIngress() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string param certificateName string @@ -1345,12 +1347,12 @@ param customDomain string { name: customDomain bindingType: (certificateName != '') ? 'SniEnabled' : 'Disabled' - certificateId: (certificateName != '') ? '${outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName}' : null + certificateId: (certificateName != '') ? '${env_outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName}' : null } ] } } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -1378,7 +1380,7 @@ public async Task ConfigureDuplicateCustomDomainMutatesIngress() var initialCertificateName = builder.AddParameter("initialCertificateName"); var expectedCertificateName = builder.AddParameter("expectedCertificateName"); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint(targetPort: 1111) .PublishAsAzureContainerApp((module, c) => @@ -1411,8 +1413,8 @@ public async Task ConfigureDuplicateCustomDomainMutatesIngress() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", "initialCertificateName": "{initialCertificateName.value}", "customDomain": "{customDomain.value}", "expectedCertificateName": "{expectedCertificateName.value}" @@ -1427,9 +1429,9 @@ public async Task ConfigureDuplicateCustomDomainMutatesIngress() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string param initialCertificateName string @@ -1451,12 +1453,12 @@ param expectedCertificateName string { name: customDomain bindingType: (expectedCertificateName != '') ? 'SniEnabled' : 'Disabled' - certificateId: (expectedCertificateName != '') ? '${outputs_azure_container_apps_environment_id}/managedCertificates/${expectedCertificateName}' : null + certificateId: (expectedCertificateName != '') ? '${env_outputs_azure_container_apps_environment_id}/managedCertificates/${expectedCertificateName}' : null } ] } } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -1486,7 +1488,7 @@ public async Task ConfigureMultipleCustomDomainsMutatesIngress() var customDomain2 = builder.AddParameter("customDomain2"); var certificateName2 = builder.AddParameter("certificateName2"); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint(targetPort: 1111) .PublishAsAzureContainerApp((module, c) => @@ -1519,8 +1521,8 @@ public async Task ConfigureMultipleCustomDomainsMutatesIngress() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", "certificateName1": "{certificateName1.value}", "customDomain1": "{customDomain1.value}", "certificateName2": "{certificateName2.value}", @@ -1536,9 +1538,9 @@ public async Task ConfigureMultipleCustomDomainsMutatesIngress() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string param certificateName1 string @@ -1562,17 +1564,17 @@ param customDomain2 string { name: customDomain1 bindingType: (certificateName1 != '') ? 'SniEnabled' : 'Disabled' - certificateId: (certificateName1 != '') ? '${outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName1}' : null + certificateId: (certificateName1 != '') ? '${env_outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName1}' : null } { name: customDomain2 bindingType: (certificateName2 != '') ? 'SniEnabled' : 'Disabled' - certificateId: (certificateName2 != '') ? '${outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName2}' : null + certificateId: (certificateName2 != '') ? '${env_outputs_azure_container_apps_environment_id}/managedCertificates/${certificateName2}' : null } ] } } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -1596,7 +1598,7 @@ public async Task VolumesAndBindMountsAreTranslation() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithVolume("vol1", "/path1") @@ -1627,11 +1629,11 @@ public async Task VolumesAndBindMountsAreTranslation() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "api_volumes_0_storage": "{api.volumes.0.storage}", - "api_volumes_1_storage": "{api.volumes.1.storage}", - "api_bindmounts_0_storage": "{api.bindMounts.0.storage}", - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" + "env_outputs_volumes_api_0": "{env.outputs.volumes_api_0}", + "env_outputs_volumes_api_1": "{env.outputs.volumes_api_1}", + "env_outputs_bindmounts_api_0": "{env.outputs.bindmounts_api_0}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" } } """; @@ -1642,17 +1644,17 @@ public async Task VolumesAndBindMountsAreTranslation() """ @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - - param api_volumes_0_storage string - - param api_volumes_1_storage string - - param api_bindmounts_0_storage string - - param outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string - + param env_outputs_volumes_api_0 string + + param env_outputs_volumes_api_1 string + + param env_outputs_bindmounts_api_0 string + + param env_outputs_azure_container_apps_environment_default_domain string + + param env_outputs_azure_container_apps_environment_id string + resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'api' location: location @@ -1660,7 +1662,7 @@ param outputs_azure_container_apps_environment_id string configuration: { activeRevisionsMode: 'Single' } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -1689,24 +1691,24 @@ param outputs_azure_container_apps_environment_id string { name: 'v0' storageType: 'AzureFile' - storageName: api_volumes_0_storage + storageName: env_outputs_volumes_api_0 } { name: 'v1' storageType: 'AzureFile' - storageName: api_volumes_1_storage + storageName: env_outputs_volumes_api_1 } { name: 'bm0' storageType: 'AzureFile' - storageName: api_bindmounts_0_storage + storageName: env_outputs_bindmounts_api_0 } ] } } } """; - + output.WriteLine(bicep); Assert.Equal(expectedBicep, bicep); } @@ -1715,7 +1717,9 @@ public async Task SecretOutputHandling() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); +#pragma warning disable CS0618 // Type or member is obsolete builder.AddAzureContainerAppsInfrastructure(); +#pragma warning restore CS0618 // Type or member is obsolete var db = builder.AddAzureCosmosDB("mydb").WithAccessKeyAuthentication(); db.AddCosmosDatabase("db"); @@ -1951,7 +1955,7 @@ public async Task CanCustomizeWithProvisioningBuildOptions() var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); builder.Services.Configure(options => options.ProvisioningBuildOptions.InfrastructureResolvers.Insert(0, new MyResourceNamePropertyResolver())); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api1", "myimage"); @@ -1976,9 +1980,9 @@ public async Task CanCustomizeWithProvisioningBuildOptions() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string resource api1 'Microsoft.App/containerApps@2024-03-01' = { name: 'api1-my' @@ -1987,7 +1991,7 @@ param outputs_azure_container_apps_environment_id string configuration: { activeRevisionsMode: 'Single' } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2024,7 +2028,7 @@ public async Task ExternalEndpointBecomesIngress() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint() @@ -2054,8 +2058,8 @@ public async Task ExternalEndpointBecomesIngress() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" } } """; @@ -2067,9 +2071,9 @@ public async Task ExternalEndpointBecomesIngress() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'api' @@ -2083,7 +2087,7 @@ param outputs_azure_container_apps_environment_id string transport: 'http' } } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2107,7 +2111,7 @@ public async Task FirstHttpEndpointBecomesIngress() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint(name: "one", targetPort: 8080) @@ -2137,8 +2141,8 @@ public async Task FirstHttpEndpointBecomesIngress() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" } } """; @@ -2150,9 +2154,9 @@ public async Task FirstHttpEndpointBecomesIngress() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'api' @@ -2172,7 +2176,7 @@ param outputs_azure_container_apps_environment_id string ] } } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2196,7 +2200,7 @@ public async Task EndpointWithHttp2SetsTransportToH2() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint() @@ -2227,8 +2231,8 @@ public async Task EndpointWithHttp2SetsTransportToH2() "type": "azure.bicep.v0", "path": "api.module.bicep", "params": { - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" } } """; @@ -2240,9 +2244,9 @@ public async Task EndpointWithHttp2SetsTransportToH2() @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'api' @@ -2256,7 +2260,7 @@ param outputs_azure_container_apps_environment_id string transport: 'http2' } } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2280,7 +2284,7 @@ public async Task ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddProject("api", launchProfileName: null) .WithHttpEndpoint() @@ -2311,10 +2315,10 @@ public async Task ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint() "path": "api.module.bicep", "params": { "api_containerport": "{api.containerPort}", - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -2329,13 +2333,13 @@ public async Task ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint() param api_containerport string - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param env_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param env_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -2352,12 +2356,12 @@ param api_containerimage string } registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2399,7 +2403,7 @@ param api_containerimage string identity: { type: 'UserAssigned' userAssignedIdentities: { - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -2413,7 +2417,7 @@ public async Task RoleAssignmentsWithAsExisting() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var storageName = builder.AddParameter("storageName"); var storageRG = builder.AddParameter("storageRG"); @@ -2453,10 +2457,10 @@ public async Task RoleAssignmentsWithAsExisting() "params": { "api_identity_outputs_id": "{api-identity.outputs.id}", "api_identity_outputs_clientid": "{api-identity.outputs.clientId}", - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -2499,13 +2503,13 @@ param api_identity_outputs_id string param api_identity_outputs_clientid string - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param env_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param env_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -2517,12 +2521,12 @@ param api_containerimage string activeRevisionsMode: 'Single' registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2557,7 +2561,7 @@ param api_containerimage string type: 'UserAssigned' userAssignedIdentities: { '${api_identity_outputs_id}': { } - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -2618,7 +2622,7 @@ public async Task RoleAssignmentsWithAsExistingCosmosDB() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var cosmosName = builder.AddParameter("cosmosName"); var cosmosRG = builder.AddParameter("cosmosRG"); @@ -2658,10 +2662,10 @@ public async Task RoleAssignmentsWithAsExistingCosmosDB() "api_identity_outputs_id": "{api-identity.outputs.id}", "api_identity_outputs_clientid": "{api-identity.outputs.clientId}", "cosmos_outputs_connectionstring": "{cosmos.outputs.connectionString}", - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -2706,13 +2710,13 @@ param api_identity_outputs_clientid string param cosmos_outputs_connectionstring string - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param env_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param env_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -2724,12 +2728,12 @@ param api_containerimage string activeRevisionsMode: 'Single' registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2768,7 +2772,7 @@ param api_containerimage string type: 'UserAssigned' userAssignedIdentities: { '${api_identity_outputs_id}': { } - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -2834,7 +2838,7 @@ public async Task RoleAssignmentsWithAsExistingRedis() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var redis = builder.AddAzureRedis("redis") .PublishAsExisting("myredis", "myRG"); @@ -2871,10 +2875,10 @@ public async Task RoleAssignmentsWithAsExistingRedis() "api_identity_outputs_id": "{api-identity.outputs.id}", "api_identity_outputs_clientid": "{api-identity.outputs.clientId}", "redis_outputs_connectionstring": "{redis.outputs.connectionString}", - "outputs_azure_container_apps_environment_default_domain": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", - "outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", - "outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", - "outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", "api_containerimage": "{api.containerImage}" } } @@ -2920,13 +2924,13 @@ param api_identity_outputs_clientid string param redis_outputs_connectionstring string - param outputs_azure_container_apps_environment_default_domain string + param env_outputs_azure_container_apps_environment_default_domain string - param outputs_azure_container_apps_environment_id string + param env_outputs_azure_container_apps_environment_id string - param outputs_azure_container_registry_endpoint string + param env_outputs_azure_container_registry_endpoint string - param outputs_azure_container_registry_managed_identity_id string + param env_outputs_azure_container_registry_managed_identity_id string param api_containerimage string @@ -2938,12 +2942,12 @@ param api_containerimage string activeRevisionsMode: 'Single' registries: [ { - server: outputs_azure_container_registry_endpoint - identity: outputs_azure_container_registry_managed_identity_id + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id } ] } - environmentId: outputs_azure_container_apps_environment_id + environmentId: env_outputs_azure_container_apps_environment_id template: { containers: [ { @@ -2982,7 +2986,7 @@ param api_containerimage string type: 'UserAssigned' userAssignedIdentities: { '${api_identity_outputs_id}': { } - '${outputs_azure_container_registry_managed_identity_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } } } } @@ -3045,7 +3049,7 @@ public async Task NonTcpHttpOrUdpSchemeThrows() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithEndpoint(scheme: "foo"); @@ -3064,7 +3068,7 @@ public async Task MultipleExternalEndpointsAreNotSupported() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint(name: "ep1") @@ -3085,7 +3089,7 @@ public async Task ExternalNonHttpEndpointsAreNotSupported() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithEndpoint("ep1", e => e.IsExternal = true); @@ -3104,7 +3108,7 @@ public async Task HttpAndTcpEndpointsCannotHaveTheSameTargetPort() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint(targetPort: 80) @@ -3124,7 +3128,7 @@ public async Task DefaultHttpIngressMustUsePort80() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpEndpoint(port: 8081); @@ -3143,7 +3147,7 @@ public async Task DefaultHttpsIngressMustUsePort443() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); builder.AddContainer("api", "myimage") .WithHttpsEndpoint(port: 8081); @@ -3178,7 +3182,9 @@ public async Task KnownParametersAreNotSetWhenUsingAzdResources() { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); +#pragma warning disable CS0618 // Type or member is obsolete builder.AddAzureContainerAppsInfrastructure(); +#pragma warning restore CS0618 // Type or member is obsolete var pg = builder.AddAzurePostgresFlexibleServer("pg") .WithPasswordAuthentication() diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs index 4c5475efbec..e9b5dbd176f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs @@ -189,7 +189,7 @@ public async Task AddAzureCosmosDB(bool useAcaInfrastructure) if (useAcaInfrastructure) { - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); } var cosmos = builder.AddAzureCosmosDB("cosmos"); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs index cc5caa95897..1fcc0525a57 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs @@ -288,7 +288,7 @@ public async Task AddAzureFunctionsProject_CanGetStorageManifestSuccessfully() public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrastructure() { using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); // hardcoded sha256 to make the storage name deterministic builder.Configuration["AppHost:Sha256"] = "634f8"; @@ -378,7 +378,7 @@ param principalId string public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrastructure_WithHostStorage() { using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); // hardcoded sha256 to make the storage name deterministic var storage = builder.AddAzureStorage("my-own-storage").RunAsEmulator(); @@ -460,7 +460,7 @@ param principalId string public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrastructure_WithHostStorage_WithRoleAssignments() { using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); // hardcoded sha256 to make the storage name deterministic var storage = builder.AddAzureStorage("my-own-storage").RunAsEmulator(); @@ -523,7 +523,7 @@ param principalId string public async Task MultipleAddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrastructure_WithHostStorage_WithRoleAssignments() { using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); // hardcoded sha256 to make the storage name deterministic var storage = builder.AddAzureStorage("my-own-storage").RunAsEmulator(); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs index cc011572937..2d530ff2f1c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs @@ -24,7 +24,7 @@ public async Task AddAzurePostgresFlexibleServer(bool publishMode, bool useAcaIn if (useAcaInfrastructure) { - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); // on ACA infrastructure, if there are no references to the postgres resource, // then there won't be any roles created. So add a reference here. diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs index bc3d6481fda..4a6ffe33f3f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs @@ -25,7 +25,7 @@ public async Task AddAzureRedis(bool useAcaInfrastructure) if (useAcaInfrastructure) { - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); } var redis = builder.AddAzureRedis("redis-cache"); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs index d936b80dd07..d4d06b650b3 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs @@ -39,7 +39,7 @@ public async Task AppliesDefaultRoleAssignmentsInRunModeIfReferenced(bool addCon using var builder = TestDistributedApplicationBuilder.Create(operation); if (addContainerAppsInfra) { - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); } var storage = builder.AddAzureStorage("storage"); @@ -125,7 +125,7 @@ param principalId string public async Task AppliesRoleAssignmentsInRunMode(DistributedApplicationOperation operation) { using var builder = TestDistributedApplicationBuilder.Create(operation); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var storage = builder.AddAzureStorage("storage"); var blobs = storage.AddBlobs("blobs"); @@ -216,7 +216,7 @@ param principalId string public async Task FindsAzureReferencesFromArguments() { using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); var storage = builder.AddAzureStorage("storage"); var blobs = storage.AddBlobs("blobs"); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs index 0b00ea47faf..d352f1a6a80 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs @@ -26,7 +26,7 @@ public async Task AddAzureSqlServer(bool publishMode, bool useAcaInfrastructure) if (useAcaInfrastructure) { - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); // on ACA infrastructure, if there are no references to the resource, // then there won't be any roles created. So add a reference here. diff --git a/tests/Aspire.Hosting.Azure.Tests/PublicApiTests/AppContainersPublicApiTests.cs b/tests/Aspire.Hosting.Azure.Tests/PublicApiTests/AppContainersPublicApiTests.cs index c4385ae8c75..13be4479554 100644 --- a/tests/Aspire.Hosting.Azure.Tests/PublicApiTests/AppContainersPublicApiTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/PublicApiTests/AppContainersPublicApiTests.cs @@ -79,7 +79,9 @@ public void AddAzureContainerAppsInfrastructureShouldThrowWhenBuilderIsNull() var action = () => { +#pragma warning disable CS0618 // Type or member is obsolete builder.AddAzureContainerAppsInfrastructure(); +#pragma warning restore CS0618 // Type or member is obsolete }; var exception = Assert.Throws(action); diff --git a/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs b/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs index 699107183b2..693ec6e0e35 100644 --- a/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs @@ -469,7 +469,7 @@ private async Task RoleAssignmentTest( bool includePrincipalName = false) { var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); - builder.AddAzureContainerAppsInfrastructure(); + builder.AddAzureContainerAppEnvironment("env"); configureBuilder(builder); From 3efff721eaaf8764461847bf0bd64910c6654402 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:48:32 -0600 Subject: [PATCH 25/30] [release/9.2] Introduce version selector for Aspire templates (#8652) * Introduce version selecto to aspire new * Update src/Aspire.Cli/Commands/NewCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Aspire.Cli/Commands/NewCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Mitch Denny Co-authored-by: David Fowler Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Commands/AddCommand.cs | 4 +-- src/Aspire.Cli/Commands/NewCommand.cs | 31 ++++++++++++------------ src/Aspire.Cli/NuGetPackageCache.cs | 22 +++++++++++++---- src/Aspire.Cli/Utils/InteractionUtils.cs | 18 ++++++++++++++ 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs index be68a9c02b6..291da3ad965 100644 --- a/src/Aspire.Cli/Commands/AddCommand.cs +++ b/src/Aspire.Cli/Commands/AddCommand.cs @@ -66,9 +66,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var source = parseResult.GetValue("--source"); - var packages = await AnsiConsole.Status().StartAsync( + var packages = await InteractionUtils.ShowStatusAsync( "Searching for Aspire packages...", - context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile.Directory!, prerelease, source, cancellationToken) + () => _nuGetPackageCache.GetIntegrationPackagesAsync(effectiveAppHostProjectFile.Directory!, prerelease, source, cancellationToken) ); var version = parseResult.GetValue("--version"); diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 2b81943a465..44a0de26cec 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -4,7 +4,6 @@ using System.CommandLine; using System.Diagnostics; using Aspire.Cli.Utils; -using Semver; using Spectre.Console; namespace Aspire.Cli.Commands; @@ -43,6 +42,10 @@ public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) var templateVersionOption = new Option("--version", "-v"); templateVersionOption.Description = "The version of the project templates to use."; Options.Add(templateVersionOption); + + var prereleaseOption = new Option("--prerelease"); + prereleaseOption.Description = "Include prerelease versions when searching for project templates."; + Options.Add(prereleaseOption); } private static async Task<(string TemplateName, string TemplateDescription, string? PathAppendage)> GetProjectTemplateAsync(ParseResult parseResult, CancellationToken cancellationToken) @@ -106,7 +109,7 @@ private static async Task GetOutputPathAsync(ParseResult parseResult, st return Path.GetFullPath(outputPath); } - private static async Task GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken) + private async Task GetProjectTemplatesVersionAsync(ParseResult parseResult, bool prerelease, string? source, CancellationToken cancellationToken) { if (parseResult.GetValue("--version") is { } version) { @@ -114,20 +117,15 @@ private static async Task GetProjectTemplatesVersionAsync(ParseResult pa } else { - version = await InteractionUtils.PromptForStringAsync( - "Project templates version:", - defaultValue: VersionHelper.GetDefaultTemplateVersion(), - validator: (string value) => { - if (SemVersion.TryParse(value, out var parsedVersion)) - { - return ValidationResult.Success(); - } - - return ValidationResult.Error("Invalid version format. Please enter a valid version."); - }, - cancellationToken); + var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory); - return version; + var candidatePackages = await InteractionUtils.ShowStatusAsync( + "Searching for available project template versions...", + () => _nuGetPackageCache.GetTemplatePackagesAsync(workingDirectory, prerelease, source, cancellationToken) + ); + + var selectedPackage = await InteractionUtils.PromptForTemplatesVersionAsync(candidatePackages, cancellationToken); + return selectedPackage.Version; } } @@ -138,8 +136,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var template = await GetProjectTemplateAsync(parseResult, cancellationToken); var name = await GetProjectNameAsync(parseResult, cancellationToken); var outputPath = await GetOutputPathAsync(parseResult, template.PathAppendage, cancellationToken); - var version = await GetProjectTemplatesVersionAsync(parseResult, cancellationToken); + var prerelease = parseResult.GetValue("--prerelease"); var source = parseResult.GetValue("--source"); + var version = await GetProjectTemplatesVersionAsync(parseResult, prerelease, source, cancellationToken); var templateInstallResult = await AnsiConsole.Status() .Spinner(Spinner.Known.Dots3) diff --git a/src/Aspire.Cli/NuGetPackageCache.cs b/src/Aspire.Cli/NuGetPackageCache.cs index 402c99a9f92..ec3a7de7c15 100644 --- a/src/Aspire.Cli/NuGetPackageCache.cs +++ b/src/Aspire.Cli/NuGetPackageCache.cs @@ -8,7 +8,8 @@ namespace Aspire.Cli; internal interface INuGetPackageCache { - Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken); + Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken); + Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken); } internal sealed class NuGetPackageCache(ILogger logger, DotNetCliRunner cliRunner) : INuGetPackageCache @@ -17,7 +18,17 @@ internal sealed class NuGetPackageCache(ILogger logger, DotNe private const int SearchPageSize = 100; - public async Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken) + public async Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken) + { + return await GetPackagesAsync(workingDirectory, "Aspire.ProjectTemplates", prerelease, source, cancellationToken); + } + + public async Task> GetIntegrationPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken) + { + return await GetPackagesAsync(workingDirectory, "Aspire.Hosting", prerelease, source, cancellationToken); + } + + internal async Task> GetPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, string? source, CancellationToken cancellationToken) { using var activity = _activitySource.StartActivity(); @@ -32,7 +43,7 @@ public async Task> GetPackagesAsync(DirectoryInfo work // This search should pick up Aspire.Hosting.* and CommunityToolkit.Aspire.Hosting.* var result = await cliRunner.SearchPackagesAsync( workingDirectory, - "Aspire.Hosting", + query, prerelease, SearchPageSize, skip, @@ -69,8 +80,9 @@ public async Task> GetPackagesAsync(DirectoryInfo work static bool IsOfficialOrCommunityToolkitPackage(string packageName) { - var isHostingOrCommunityToolkitNamespaced = packageName.StartsWith("Aspire.Hosting.", StringComparison.OrdinalIgnoreCase) || - packageName.StartsWith("CommunityToolkit.Aspire.Hosting.", StringComparison.OrdinalIgnoreCase); + var isHostingOrCommunityToolkitNamespaced = packageName.StartsWith("Aspire.Hosting.", StringComparison.Ordinal) || + packageName.StartsWith("CommunityToolkit.Aspire.Hosting.", StringComparison.Ordinal) || + packageName.Equals("Aspire.ProjectTemplates", StringComparison.Ordinal); var isExcluded = packageName.StartsWith("Aspire.Hosting.AppHost") || packageName.StartsWith("Aspire.Hosting.Sdk") || diff --git a/src/Aspire.Cli/Utils/InteractionUtils.cs b/src/Aspire.Cli/Utils/InteractionUtils.cs index cf517af303e..900fc5faa36 100644 --- a/src/Aspire.Cli/Utils/InteractionUtils.cs +++ b/src/Aspire.Cli/Utils/InteractionUtils.cs @@ -8,6 +8,24 @@ namespace Aspire.Cli.Utils; internal static class InteractionUtils { + public static async Task ShowStatusAsync(string statusText, Func> action) + { + return await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync(statusText, (context) => action()); + } + + public static async Task PromptForTemplatesVersionAsync(IEnumerable candidatePackages, CancellationToken cancellationToken) + { + return await PromptForSelectionAsync( + "Select a template version:", + candidatePackages, + (p) => $"{p.Version} ({p.Source})", + cancellationToken + ); + } + public static async Task PromptForStringAsync(string promptText, string? defaultValue = null, Func? validator = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(promptText, nameof(promptText)); From fdc6ab51a55a14ce0b4d79e72d9b417af8aa53db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:49:00 -0600 Subject: [PATCH 26/30] [release/9.2] Error handling for GetCapabilitiesAsync connection issues (#8651) * Error out when GetCapabiltiesAsync is missing or unexpected exception occurs. * Update src/Aspire.Cli/DotNetCliRunner.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Mitch Denny Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Backchannel/AppHostBackchannel.cs | 59 +++++++++++-------- src/Aspire.Cli/DotNetCliRunner.cs | 6 ++ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs index c605bcf02e0..b745e380131 100644 --- a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs @@ -88,38 +88,49 @@ await rpc.InvokeWithCancellationAsync( public async Task ConnectAsync(Process process, string socketPath, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(); - - _process = process; - - if (_rpcTaskCompletionSource.Task.IsCompleted) + try { - throw new InvalidOperationException("Already connected to AppHost backchannel."); + using var activity = _activitySource.StartActivity(); + + _process = process; + + if (_rpcTaskCompletionSource.Task.IsCompleted) + { + throw new InvalidOperationException("Already connected to AppHost backchannel."); + } + + logger.LogDebug("Connecting to AppHost backchannel at {SocketPath}", socketPath); + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + var endpoint = new UnixDomainSocketEndPoint(socketPath); + await socket.ConnectAsync(endpoint, cancellationToken); + logger.LogDebug("Connected to AppHost backchannel at {SocketPath}", socketPath); + + var stream = new NetworkStream(socket, true); + var rpc = JsonRpc.Attach(stream, target); + + var capabilities = await rpc.InvokeWithCancellationAsync( + "GetCapabilitiesAsync", + Array.Empty(), + cancellationToken); + + if (!capabilities.Any(s => s == "baseline.v0")) + { + throw new AppHostIncompatibleException( + $"AppHost is incompatible with the CLI. The AppHost must be updated to a version that supports the baseline.v0 capability.", + "baseline.v0" + ); + } + + _rpcTaskCompletionSource.SetResult(rpc); } - - logger.LogDebug("Connecting to AppHost backchannel at {SocketPath}", socketPath); - var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - var endpoint = new UnixDomainSocketEndPoint(socketPath); - await socket.ConnectAsync(endpoint, cancellationToken); - logger.LogDebug("Connected to AppHost backchannel at {SocketPath}", socketPath); - - var stream = new NetworkStream(socket, true); - var rpc = JsonRpc.Attach(stream, target); - - var capabilities = await rpc.InvokeWithCancellationAsync( - "GetCapabilitiesAsync", - Array.Empty(), - cancellationToken); - - if (!capabilities.Any(s => s == "baseline.v0")) + catch (RemoteMethodNotFoundException ex) { + logger.LogError(ex, "Failed to connect to AppHost backchannel. The AppHost must be updated to a version that supports the baseline.v0 capability."); throw new AppHostIncompatibleException( $"AppHost is incompatible with the CLI. The AppHost must be updated to a version that supports the baseline.v0 capability.", "baseline.v0" ); } - - _rpcTaskCompletionSource.SetResult(rpc); } public async Task GetPublishersAsync(CancellationToken cancellationToken) diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index e8d947d1f12..97d318fb952 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -486,6 +486,12 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas throw; } + catch (Exception ex) + { + logger.LogError(ex, "An unexpected error occurred while trying to connect to the backchannel."); + backchannelCompletionSource.SetException(ex); + throw; + } } while (await timer.WaitForNextTickAsync(cancellationToken)); } From adcd3e6633b018d6b67b9922760b1769c60e422c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:23:32 -0700 Subject: [PATCH 27/30] Update dependencies from https://github.com/microsoft/usvc-apiserver build 0.12.11 (#8660) Microsoft.DeveloperControlPlane.darwin-amd64 , Microsoft.DeveloperControlPlane.darwin-arm64 , Microsoft.DeveloperControlPlane.linux-amd64 , Microsoft.DeveloperControlPlane.linux-arm64 , Microsoft.DeveloperControlPlane.windows-386 , Microsoft.DeveloperControlPlane.windows-amd64 , Microsoft.DeveloperControlPlane.windows-arm64 From Version 0.12.9 -> To Version 0.12.11 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 28 ++++++++++++++-------------- eng/Versions.props | 14 +++++++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 595dd987f63..f7e44a36a61 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,33 +1,33 @@ - + https://github.com/microsoft/usvc-apiserver - b9b5f5f43244f9b0f8d13de2cd699b90c69b75c7 + 064e816df88f98f115c2cb065dd9d2f070d59d79 - + https://github.com/microsoft/usvc-apiserver - b9b5f5f43244f9b0f8d13de2cd699b90c69b75c7 + 064e816df88f98f115c2cb065dd9d2f070d59d79 - + https://github.com/microsoft/usvc-apiserver - b9b5f5f43244f9b0f8d13de2cd699b90c69b75c7 + 064e816df88f98f115c2cb065dd9d2f070d59d79 - + https://github.com/microsoft/usvc-apiserver - b9b5f5f43244f9b0f8d13de2cd699b90c69b75c7 + 064e816df88f98f115c2cb065dd9d2f070d59d79 - + https://github.com/microsoft/usvc-apiserver - b9b5f5f43244f9b0f8d13de2cd699b90c69b75c7 + 064e816df88f98f115c2cb065dd9d2f070d59d79 - + https://github.com/microsoft/usvc-apiserver - b9b5f5f43244f9b0f8d13de2cd699b90c69b75c7 + 064e816df88f98f115c2cb065dd9d2f070d59d79 - + https://github.com/microsoft/usvc-apiserver - b9b5f5f43244f9b0f8d13de2cd699b90c69b75c7 + 064e816df88f98f115c2cb065dd9d2f070d59d79 https://dev.azure.com/dnceng/internal/_git/dotnet-extensions diff --git a/eng/Versions.props b/eng/Versions.props index 820ab4b4ef2..a110fdc869e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -24,13 +24,13 @@ 8.0.100-rtm.23512.16 - 0.12.9 - 0.12.9 - 0.12.9 - 0.12.9 - 0.12.9 - 0.12.9 - 0.12.9 + 0.12.11 + 0.12.11 + 0.12.11 + 0.12.11 + 0.12.11 + 0.12.11 + 0.12.11 9.0.0-beta.25164.2 10.0.0-beta.25178.1 From b340e2d6d17ce48047945577eb56fb8caeed4f00 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 8 Apr 2025 20:08:20 -0700 Subject: [PATCH 28/30] Updating versions of .NET Servicing and Extensions and stabilize packages for 9.2 (#8654) * Updating versions of .NET Servicing and Extensions * Fix EndToEnd tests --- eng/Version.Details.xml | 74 ++++----- eng/Versions.props | 88 +++++------ .../RepoTesting/Aspire.RepoTesting.targets | 4 +- .../Directory.Packages.Helix.props | 140 +++++++++--------- 4 files changed, 153 insertions(+), 153 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index f7e44a36a61..75dee48a8f1 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -29,17 +29,17 @@ https://github.com/microsoft/usvc-apiserver 064e816df88f98f115c2cb065dd9d2f070d59d79 - + https://dev.azure.com/dnceng/internal/_git/dotnet-extensions - f1f17e642a685df7e87b805be1efe4729ff725e4 + 7e7b3eeb834955d0c0676cb788d0b9b3c949b776 - + https://dev.azure.com/dnceng/internal/_git/dotnet-extensions - f1f17e642a685df7e87b805be1efe4729ff725e4 + 7e7b3eeb834955d0c0676cb788d0b9b3c949b776 - + https://dev.azure.com/dnceng/internal/_git/dotnet-extensions - f1f17e642a685df7e87b805be1efe4729ff725e4 + 7e7b3eeb834955d0c0676cb788d0b9b3c949b776 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -67,7 +67,7 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - eba546b0f0d448e0176a2222548fd7a2fbf464c0 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -77,61 +77,61 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 9cb3b725e3ad2b57ddc9fb2dd48d2d170563a8f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 @@ -139,9 +139,9 @@ eb436a482c2a0c6aa45578ea0a48fd3782c275b2 - + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore - 25ef4aa38b77974263cb10f6e9cbd10135f17b59 + 8899cb30120d41413065f1b1465cdabefe0a1f9c diff --git a/eng/Versions.props b/eng/Versions.props index a110fdc869e..72d3d602466 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -17,7 +17,7 @@ 1.20.0 3.0.2 - false + true release @@ -37,44 +37,44 @@ 9.0.0-beta.25164.2 9.0.0-beta.25164.2 - 9.4.0-preview.1.25202.3 + 9.4.0-preview.1.25207.5 - 9.3.0 - 9.3.0 - 9.3.0 + 9.4.0 + 9.4.0 + 9.4.0 9.0.2 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 1.11.2 1.11.1 @@ -86,21 +86,21 @@ - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 8.0.1 8.0.1 diff --git a/tests/Shared/RepoTesting/Aspire.RepoTesting.targets b/tests/Shared/RepoTesting/Aspire.RepoTesting.targets index deb1a291271..0b637658686 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/` --> - + @@ -151,6 +151,6 @@ $(MajorVersion).$(MinorVersion).$(PatchVersion) - + diff --git a/tests/Shared/RepoTesting/Directory.Packages.Helix.props b/tests/Shared/RepoTesting/Directory.Packages.Helix.props index a36826bcfe9..980cea21843 100644 --- a/tests/Shared/RepoTesting/Directory.Packages.Helix.props +++ b/tests/Shared/RepoTesting/Directory.Packages.Helix.props @@ -3,85 +3,85 @@ - - - - - - - - - - + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + From e686aceb3fa5b92c744eb98a39683acb0c917eea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:04:50 -0700 Subject: [PATCH 29/30] Remove -w short option for --wait-for-debugger. (#8663) Co-authored-by: Mitch Denny --- src/Aspire.Cli/Commands/RootCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Cli/Commands/RootCommand.cs b/src/Aspire.Cli/Commands/RootCommand.cs index 22d4ad6bbdf..326c3d68ff3 100644 --- a/src/Aspire.Cli/Commands/RootCommand.cs +++ b/src/Aspire.Cli/Commands/RootCommand.cs @@ -22,7 +22,7 @@ public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addC debugOption.Recursive = true; Options.Add(debugOption); - var waitForDebuggerOption = new Option("--wait-for-debugger", "-w"); + var waitForDebuggerOption = new Option("--wait-for-debugger"); waitForDebuggerOption.Description = "Wait for a debugger to attach before executing the command."; waitForDebuggerOption.Recursive = true; waitForDebuggerOption.DefaultValueFactory = (result) => false; From 0fcb1e9885266c1700c49c16513a6d97480bb058 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 08:39:14 -0600 Subject: [PATCH 30/30] Mark DistributedApplicationOperation.Inspect as experimental, and imrpove AddPublisher API. (#8666) Co-authored-by: Mitch Denny --- src/Aspire.Hosting.Azure/AzurePublisherExtensions.cs | 8 ++++---- .../DockerComposePublisherExtensions.cs | 8 ++++---- .../KubernetesPublisherExtensions.cs | 8 ++++---- .../DistributedApplicationExecutionContext.cs | 3 +++ src/Aspire.Hosting/DistributedApplicationOperation.cs | 3 +++ .../PublisherDistributedApplicationBuilderExtensions.cs | 4 +++- tests/Aspire.Hosting.Tests/OperationModesTests.cs | 2 ++ 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/Aspire.Hosting.Azure/AzurePublisherExtensions.cs b/src/Aspire.Hosting.Azure/AzurePublisherExtensions.cs index 1f22bb68a23..4cab3815997 100644 --- a/src/Aspire.Hosting.Azure/AzurePublisherExtensions.cs +++ b/src/Aspire.Hosting.Azure/AzurePublisherExtensions.cs @@ -18,9 +18,9 @@ public static class AzurePublisherExtensions /// The name of the publisher used when using the Aspire CLI. /// Callback to configure Azure Container Apps publisher options. [Experimental("ASPIREPUBLISHERS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] - public static void AddAzurePublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) + public static IDistributedApplicationBuilder AddAzurePublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) { - builder.AddPublisher(name, configureOptions); + return builder.AddPublisher(name, configureOptions); } /// @@ -29,8 +29,8 @@ public static void AddAzurePublisher(this IDistributedApplicationBuilder builder /// The . /// Callback to configure Azure Container Apps publisher options. [Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] - public static void AddAzurePublisher(this IDistributedApplicationBuilder builder, Action? configureOptions = null) + public static IDistributedApplicationBuilder AddAzurePublisher(this IDistributedApplicationBuilder builder, Action? configureOptions = null) { - builder.AddPublisher("azure", configureOptions); + return builder.AddPublisher("azure", configureOptions); } } \ No newline at end of file diff --git a/src/Aspire.Hosting.Docker/DockerComposePublisherExtensions.cs b/src/Aspire.Hosting.Docker/DockerComposePublisherExtensions.cs index 0e85a876050..8f0f4504f93 100644 --- a/src/Aspire.Hosting.Docker/DockerComposePublisherExtensions.cs +++ b/src/Aspire.Hosting.Docker/DockerComposePublisherExtensions.cs @@ -18,9 +18,9 @@ public static class DockerComposePublisherExtensions /// The . /// The name of the publisher used when using the Aspire CLI. /// Callback to configure Docker Compose publisher options. - public static void AddDockerComposePublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) + public static IDistributedApplicationBuilder AddDockerComposePublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) { - builder.AddPublisher(name, configureOptions); + return builder.AddPublisher(name, configureOptions); } /// @@ -28,8 +28,8 @@ public static void AddDockerComposePublisher(this IDistributedApplicationBuilder /// /// The . /// Callback to configure Docker Compose publisher options. - public static void AddDockerComposePublisher(this IDistributedApplicationBuilder builder, Action? configureOptions = null) + public static IDistributedApplicationBuilder AddDockerComposePublisher(this IDistributedApplicationBuilder builder, Action? configureOptions = null) { - builder.AddPublisher("docker-compose", configureOptions); + return builder.AddPublisher("docker-compose", configureOptions); } } diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesPublisherExtensions.cs b/src/Aspire.Hosting.Kubernetes/KubernetesPublisherExtensions.cs index 9f233dedfdd..3bb086210b3 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesPublisherExtensions.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesPublisherExtensions.cs @@ -18,9 +18,9 @@ public static class KubernetesPublisherExtensions /// The . /// The name of the publisher used when using the Aspire CLI. /// Callback to configure Kubernetes publisher options. - public static void AddKubernetesPublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) + public static IDistributedApplicationBuilder AddKubernetesPublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) { - builder.AddPublisher(name, configureOptions); + return builder.AddPublisher(name, configureOptions); } /// @@ -28,8 +28,8 @@ public static void AddKubernetesPublisher(this IDistributedApplicationBuilder bu /// /// The . /// Callback to configure Kubernetes publisher options. - public static void AddKubernetesPublisher(this IDistributedApplicationBuilder builder, Action? configureOptions = null) + public static IDistributedApplicationBuilder AddKubernetesPublisher(this IDistributedApplicationBuilder builder, Action? configureOptions = null) { - builder.AddPublisher("kubernetes", configureOptions); + return builder.AddPublisher("kubernetes", configureOptions); } } \ No newline at end of file diff --git a/src/Aspire.Hosting/DistributedApplicationExecutionContext.cs b/src/Aspire.Hosting/DistributedApplicationExecutionContext.cs index 119d0ffecdd..b230701616b 100644 --- a/src/Aspire.Hosting/DistributedApplicationExecutionContext.cs +++ b/src/Aspire.Hosting/DistributedApplicationExecutionContext.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; /// @@ -89,5 +91,6 @@ public IServiceProvider ServiceProvider /// /// Returns true if the current operation is inspecting. /// + [Experimental("ASPIREPUBLISHERS001")] public bool IsInspectMode => Operation == DistributedApplicationOperation.Inspect; } diff --git a/src/Aspire.Hosting/DistributedApplicationOperation.cs b/src/Aspire.Hosting/DistributedApplicationOperation.cs index 6cdecb8d363..80b5b8fc178 100644 --- a/src/Aspire.Hosting/DistributedApplicationOperation.cs +++ b/src/Aspire.Hosting/DistributedApplicationOperation.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; /// @@ -21,5 +23,6 @@ public enum DistributedApplicationOperation /// /// AppHost is being run for the purpose of inspecting the application model from the launcher. /// + [Experimental("ASPIREPUBLISHERS001")] Inspect } diff --git a/src/Aspire.Hosting/PublisherDistributedApplicationBuilderExtensions.cs b/src/Aspire.Hosting/PublisherDistributedApplicationBuilderExtensions.cs index c5c0bd0d795..f879ccc8938 100644 --- a/src/Aspire.Hosting/PublisherDistributedApplicationBuilderExtensions.cs +++ b/src/Aspire.Hosting/PublisherDistributedApplicationBuilderExtensions.cs @@ -21,7 +21,7 @@ public static class PublisherDistributedApplicationBuilderExtensions /// The name of the publisher. /// Callback to configure options for the publisher. [Experimental("ASPIREPUBLISHERS001")] - public static void AddPublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) + public static IDistributedApplicationBuilder AddPublisher(this IDistributedApplicationBuilder builder, string name, Action? configureOptions = null) where TPublisher : class, IDistributedApplicationPublisher where TPublisherOptions : class { @@ -43,5 +43,7 @@ public static void AddPublisher(this IDistributed { configureOptions?.Invoke(options); }); + + return builder; } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Tests/OperationModesTests.cs b/tests/Aspire.Hosting.Tests/OperationModesTests.cs index 5485846f872..9f9d0799e95 100644 --- a/tests/Aspire.Hosting.Tests/OperationModesTests.cs +++ b/tests/Aspire.Hosting.Tests/OperationModesTests.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 ASPIREPUBLISHERS001 + using Aspire.Hosting.Backchannel; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils;