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")]