diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs b/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs index dff22d0c148..86876845d09 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs @@ -3,6 +3,7 @@ #pragma warning disable ASPIREACADOMAINS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +using Aspire.Hosting.Azure; using Azure.Provisioning.Storage; var builder = DistributedApplication.CreateBuilder(args); @@ -44,6 +45,13 @@ .WithReference(redis) .WithReference(cosmosDb) .WithEnvironment("VALUE", param) + .WithEnvironment(context => + { + if (context.Resource.TryGetLastAnnotation(out var identity)) + { + context.EnvironmentVariables["AZURE_PRINCIPAL_NAME"] = identity.IdentityResource.PrincipalName; + } + }) .PublishAsAzureContainerApp((module, app) => { app.ConfigureCustomDomain(customDomain, certificateName); diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/api.module.bicep b/playground/AzureContainerApps/AzureContainerApps.AppHost/api.module.bicep index 35f82d6af1e..ac7e461d655 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/api.module.bicep +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/api.module.bicep @@ -17,6 +17,8 @@ param account_kv_outputs_name string @secure() param secretparam_value string +param api_identity_outputs_principalname string + param infra_outputs_azure_container_apps_environment_id string param infra_outputs_azure_container_registry_endpoint string @@ -121,6 +123,10 @@ resource api 'Microsoft.App/containerApps@2024-03-01' = { name: 'VALUE' secretRef: 'value' } + { + name: 'AZURE_PRINCIPAL_NAME' + value: api_identity_outputs_principalname + } { name: 'AZURE_CLIENT_ID' value: api_identity_outputs_clientid diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/aspire-manifest.json b/playground/AzureContainerApps/AzureContainerApps.AppHost/aspire-manifest.json index 18d402f2196..ee77047fa89 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/aspire-manifest.json +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/aspire-manifest.json @@ -129,6 +129,7 @@ "cache_password_value": "{cache-password.value}", "account_kv_outputs_name": "{account-kv.outputs.name}", "secretparam_value": "{secretparam.value}", + "api_identity_outputs_principalname": "{api-identity.outputs.principalName}", "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}", @@ -146,7 +147,8 @@ "ConnectionStrings__blobs": "{blobs.connectionString}", "ConnectionStrings__cache": "{cache.connectionString}", "ConnectionStrings__account": "{account.connectionString}", - "VALUE": "{secretparam.value}" + "VALUE": "{secretparam.value}", + "AZURE_PRINCIPAL_NAME": "{api-identity.outputs.principalName}" }, "bindings": { "http": { diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs index e15a1455f4a..c8f12efce62 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs @@ -524,7 +524,7 @@ private async Task ProcessEnvironmentAsync(DistributedApplicationExecutionContex { if (resource.TryGetAnnotationsOfType(out var environmentCallbacks)) { - var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken); + var context = new EnvironmentCallbackContext(executionContext, resource, cancellationToken: cancellationToken); foreach (var c in environmentCallbacks) { diff --git a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs index 9b1182447a0..a46a444290b 100644 --- a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs +++ b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs @@ -281,7 +281,7 @@ private async Task> GetAzureReferences(IResource resourc if (resource.TryGetEnvironmentVariables(out var environmentCallbacks)) { - var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken); + var context = new EnvironmentCallbackContext(executionContext, resource, cancellationToken: cancellationToken); foreach (var c in environmentCallbacks) { diff --git a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs index 1ce4a2ae4c8..9959a367cf5 100644 --- a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs +++ b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs @@ -309,7 +309,7 @@ private async Task ProcessEnvironmentAsync(DistributedApplicationExecutionContex { if (resource.TryGetAnnotationsOfType(out var environmentCallbacks)) { - var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken); + var context = new EnvironmentCallbackContext(executionContext, resource, cancellationToken: cancellationToken); foreach (var c in environmentCallbacks) { diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesResourceContext.cs b/src/Aspire.Hosting.Kubernetes/KubernetesResourceContext.cs index 914b4f2c51a..36b1842fb87 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesResourceContext.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesResourceContext.cs @@ -187,7 +187,7 @@ private async Task ProcessEnvironmentAsync(DistributedApplicationExecutionContex { if (resource.TryGetAnnotationsOfType(out var environmentCallbacks)) { - var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken); + var context = new EnvironmentCallbackContext(executionContext, resource, cancellationToken: cancellationToken); foreach (var c in environmentCallbacks) { diff --git a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs index 9b9ddbedccd..6902bfc2ad6 100644 --- a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs +++ b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs @@ -14,6 +14,21 @@ namespace Aspire.Hosting.ApplicationModel; /// A . public class EnvironmentCallbackContext(DistributedApplicationExecutionContext executionContext, Dictionary? environmentVariables = null, CancellationToken cancellationToken = default) { + private readonly IResource? _resource; + + /// + /// Initializes a new instance of the class. + /// + /// The execution context for this invocation of the AppHost. + /// The resource associated with this callback context. + /// The environment variables associated with this execution. + /// A . + public EnvironmentCallbackContext(DistributedApplicationExecutionContext executionContext, IResource resource, Dictionary? environmentVariables = null, CancellationToken cancellationToken = default) + : this(executionContext, environmentVariables, cancellationToken) + { + _resource = resource ?? throw new ArgumentNullException(nameof(resource)); + } + /// /// Gets the environment variables associated with the callback context. /// @@ -29,6 +44,15 @@ public class EnvironmentCallbackContext(DistributedApplicationExecutionContext e /// public ILogger Logger { get; set; } = NullLogger.Instance; + /// + /// The resource associated with this callback context. + /// + /// + /// This will be set to the resource in all cases where .NET Aspire invokes the callback. + /// + /// Thrown when the EnvironmentCallbackContext was created without a specified resource. + public IResource Resource => _resource ?? throw new InvalidOperationException($"{nameof(Resource)} is not set. This callback context is not associated with a resource."); + /// /// Gets the execution context associated with this invocation of the AppHost. /// diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 529b64b1c9f..d66f0cae74c 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -342,7 +342,7 @@ public static async ValueTask ProcessEnvironmentVariableValuesAsync( if (resource.TryGetEnvironmentVariables(out var callbacks)) { var config = new Dictionary(); - var context = new EnvironmentCallbackContext(executionContext, config, cancellationToken) + var context = new EnvironmentCallbackContext(executionContext, resource, config, cancellationToken) { Logger = logger }; diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index 41f875d1f13..9519e92b88f 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -131,6 +131,8 @@ public async Task HostUrlPropertyGetsResolved(bool container, string hostUrlVal, var test = builder.AddResource(new ContainerResource("testSource")) .WithEnvironment(env => { + Assert.NotNull(env.Resource); + env.EnvironmentVariables["envname"] = new HostUrl(hostUrlVal); }); diff --git a/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs b/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs index 6e86a883dda..006fedc0dc2 100644 --- a/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs @@ -170,6 +170,8 @@ public async Task GetEnvironmentVariableValuesAsyncReturnCorrectVariablesInRunMo .WithEnvironment("xpack.security.enabled", "true") .WithEnvironment(context => { + Assert.NotNull(context.Resource); + context.EnvironmentVariables["ELASTIC_PASSWORD"] = "p@ssw0rd1"; }); diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index ffe75a19850..51f8c813819 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -19,6 +19,8 @@ public async Task BuiltApplicationHasAccessToIServiceProviderViaEnvironmentCallb var container = builder.AddContainer("container", "image") .WithEnvironment(context => { + Assert.NotNull(context.Resource); + var sp = context.ExecutionContext.ServiceProvider; context.EnvironmentVariables["SP_AVAILABLE"] = sp is not null ? "true" : "false"; }); @@ -161,6 +163,32 @@ public async Task ComplexEnvironmentCallbackPopulatesValueWhenCalled() var projectA = builder.AddProject("projectA") .WithEnvironment(context => { + Assert.NotNull(context.Resource); + + context.EnvironmentVariables["myName"] = environmentValue; + }); + + environmentValue = "value2"; + + // Call environment variable callbacks. + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); + + Assert.Equal("value2", config["myName"]); + } + + [Fact] + public async Task ComplexAsyncEnvironmentCallbackPopulatesValueWhenCalled() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var environmentValue = "value"; + var projectA = builder.AddProject("projectA") + .WithEnvironment(async context => + { + await Task.Yield(); + + Assert.NotNull(context.Resource); + context.EnvironmentVariables["myName"] = environmentValue; });