Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ private static void ConfigurePublishMode<T>(
}

// Get the corresponding ContainerResource for ExecutableResources. Usually this is swapped in at publish time for ExecutableResources.
IResource target;
IResourceWithEnvironment target;
if (resource is ContainerResource containerResource)
{
target = containerResource;
Expand All @@ -308,7 +308,8 @@ private static void ConfigurePublishMode<T>(
{
// Ensure we have a container resource to deploy.
// ExecutableResource needs PublishAsDockerFile() to convert it into a container resource at this stage.
builder.ApplicationBuilder.CreateResourceBuilder(executableResource).PublishAsDockerFile();
builder.ApplicationBuilder.CreateResourceBuilder(executableResource)
.PublishAsDockerFile();

if (builder.ApplicationBuilder.TryCreateResourceBuilder(resource.Name, out containerResourceBuilder))
{
Expand All @@ -328,6 +329,11 @@ private static void ConfigurePublishMode<T>(
throw new InvalidOperationException($"Unable to create hosted agent for resource '{resource.Name}' because it is not a container, executable, or project resource.");
}

// The hosted agent wrapper is not the deployed workload. Apply the Foundry
// reference to the target so its connection annotations flow into the deployment.
builder.ApplicationBuilder.CreateResourceBuilder(target)
.WithReference(project);

// Create a separate agent resource to host the deployment.
var hostedAgent = new AzureHostedAgentResource(agentName, target, configure);

Expand All @@ -340,7 +346,6 @@ private static void ConfigurePublishMode<T>(

builder.ApplicationBuilder.AddResource(hostedAgent)
.WithIconName("Agents")
.WithReferenceRelationship(target)
.WithReference(project);
.WithReferenceRelationship(target);
}
}
14 changes: 14 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/FoundryExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ public async Task WithComputeEnvironment_ResolvesExternalContainerAppReference()
var environment = Assert.Single(model.Resources.OfType<AzureContainerAppEnvironmentResource>());
environment.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = "example.azurecontainerapps.io";
environment.ProvisioningTaskCompletionSource?.TrySetResult();
SetFoundryProjectOutputs(project.Resource);

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var environmentVariables = await AzureHostedAgentResource.GetResolvedEnvironmentVariablesAsync(
Expand Down Expand Up @@ -332,6 +333,7 @@ public async Task WithComputeEnvironment_DoesNotSetReservedFoundryProjectEndpoin

var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var hostedAgent = Assert.Single(model.Resources.OfType<AzureHostedAgentResource>());
SetFoundryProjectOutputs(project.Resource);

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var environmentVariables = await AzureHostedAgentResource.GetResolvedEnvironmentVariablesAsync(
Expand Down Expand Up @@ -372,6 +374,7 @@ public async Task WithComputeEnvironment_ResolvesReferenceExpressionEnvironmentV
var environment = Assert.Single(model.Resources.OfType<AzureContainerAppEnvironmentResource>());
environment.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = "example.azurecontainerapps.io";
environment.ProvisioningTaskCompletionSource?.TrySetResult();
SetFoundryProjectOutputs(project.Resource);

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var environmentVariables = await AzureHostedAgentResource.GetResolvedEnvironmentVariablesAsync(
Expand Down Expand Up @@ -412,6 +415,7 @@ public async Task WithComputeEnvironment_ResolvesEndpointReferenceExpressionEnvi
var environment = Assert.Single(model.Resources.OfType<AzureContainerAppEnvironmentResource>());
environment.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = "example.azurecontainerapps.io";
environment.ProvisioningTaskCompletionSource?.TrySetResult();
SetFoundryProjectOutputs(project.Resource);

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var environmentVariables = await AzureHostedAgentResource.GetResolvedEnvironmentVariablesAsync(
Expand Down Expand Up @@ -462,6 +466,16 @@ await AzureHostedAgentResource.GetResolvedEnvironmentVariablesAsync(
Assert.Contains("internal", ex.Message);
}

private static void SetFoundryProjectOutputs(AzureCognitiveServicesProjectResource project)
{
// These tests call the deployment-time environment resolver directly. In a real publish,
// provisioning populates the Foundry project Bicep outputs before references are resolved.
// Seed the outputs here so BicepOutputReference.GetValueAsync does not wait for provisioning.
project.Outputs["endpoint"] = "https://account.services.ai.azure.com/api/projects/my-project";
project.Outputs["APPLICATION_INSIGHTS_CONNECTION_STRING"] = "";
project.ProvisioningTaskCompletionSource?.TrySetResult();
}

private sealed class Project : IProjectMetadata
{
public string ProjectPath => "project";
Expand Down
39 changes: 39 additions & 0 deletions tests/Aspire.Hosting.Foundry.Tests/HostedAgentExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -142,6 +143,39 @@ public void AsHostedAgent_InPublishMode_CreatesHostedAgentResource()
Assert.Equal("agent-ha", hostedAgent.Name);
}

[Fact]
public async Task AsHostedAgent_InPublishMode_AddsProjectReferenceToDeploymentTarget()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var project = builder.AddFoundry("account")
.AddProject("my-project");

builder.AddProject<Project>("agent", launchProfileName: null)
.AsHostedAgent(project);

builder.Build();

var hostedAgent = Assert.Single(builder.Resources.OfType<AzureHostedAgentResource>());

Assert.True(hostedAgent.Target.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationships));
Assert.Contains(relationships, r =>
r.Type == "Reference" &&
ReferenceEquals(r.Resource, project.Resource));

var envVars = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
hostedAgent.Target, DistributedApplicationOperation.Publish, TestServiceProvider.Instance);

Assert.Contains(envVars, kvp =>
kvp.Key == "ConnectionStrings__my-project" &&
kvp.Value == "{my-project.connectionString}");
Assert.Contains(envVars, kvp =>
kvp.Key == "MY_PROJECT_CONNECTIONSTRING" &&
kvp.Value == "Endpoint={my-project.outputs.endpoint}");
Assert.DoesNotContain(hostedAgent.Annotations.OfType<ResourceRelationshipAnnotation>(), r =>
r.Type == "Reference" &&
ReferenceEquals(r.Resource, project.Resource));
}

[Fact]
public void AsHostedAgent_WithOptions_AppliesAllPropertiesToConfiguration()
{
Expand Down Expand Up @@ -282,4 +316,9 @@ public async Task FoundryProject_DefaultRegistryDoesNotAddGlobalRegistryTargets(

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);

private sealed class Project : IProjectMetadata
{
public string ProjectPath => "project";
}
}
Loading