Skip to content

Commit a8a72f0

Browse files
Copiloteerhardtdavidfowl
authored
Add derived Azure resource classes for accessing original compute resources (#11646)
* Initial plan * Add TargetComputeResourceAnnotation and comprehensive tests Co-authored-by: eerhardt <[email protected]> * Use cleaner TargetComputeResourceAnnotation implementation from VS Code checkpoint and add comprehensive unit tests Co-authored-by: eerhardt <[email protected]> * Rename TargetComputeResourceAnnotation to DeploymentTargetParentAnnotation Co-authored-by: eerhardt <[email protected]> * Replace annotation approach with derived resource classes as requested by @davidfowl Co-authored-by: eerhardt <[email protected]> * Fix test failures: use external endpoint for App Service and accept derived resource types Co-authored-by: davidfowl <[email protected]> * Use UnsafeAccessor for ExecuteBeforeStartHooksAsync instead of manual implementation Co-authored-by: davidfowl <[email protected]> * PR feedback * Rename --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: eerhardt <[email protected]> Co-authored-by: davidfowl <[email protected]> Co-authored-by: Eric Erhardt <[email protected]>
1 parent 46c2529 commit a8a72f0

File tree

7 files changed

+166
-4
lines changed

7 files changed

+166
-4
lines changed

Aspire.slnx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,6 @@
349349
<File Path="spelling.dic" />
350350
<File Path="src/Schema/aspire-8.0.json" />
351351
</Folder>
352-
<Folder Name="/src/" />
353352
<Folder Name="/Testing/">
354353
<Project Path="src/Aspire.Hosting.Testing/Aspire.Hosting.Testing.csproj" />
355354
</Folder>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
6+
namespace Aspire.Hosting.Azure.AppContainers;
7+
8+
/// <summary>
9+
/// Represents an Azure Container App resource.
10+
/// </summary>
11+
/// <param name="name">The name of the resource in the Aspire application model.</param>
12+
/// <param name="configureInfrastructure">Callback to configure the Azure resources.</param>
13+
/// <param name="targetResource">The target compute resource that this Azure Container App is being created for.</param>
14+
public class AzureContainerAppResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure, IResource targetResource)
15+
: AzureProvisioningResource(name, configureInfrastructure)
16+
{
17+
/// <summary>
18+
/// Gets the target resource that this Azure Container App is being created for.
19+
/// </summary>
20+
public IResource TargetResource { get; } = targetResource;
21+
}

src/Aspire.Hosting.Azure.AppContainers/ContainerAppEnvironmentContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task<AzureBicepResource> CreateContainerAppAsync(IResource resource
4343
await context.ProcessResourceAsync(cancellationToken).ConfigureAwait(false);
4444
}
4545

46-
var provisioningResource = new AzureProvisioningResource(resource.Name, context.BuildContainerApp)
46+
var provisioningResource = new AzureContainerAppResource(resource.Name, context.BuildContainerApp, resource)
4747
{
4848
ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions
4949
};

src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public async Task<AzureBicepResource> CreateAppServiceAsync(IResource resource,
4141
await context.ProcessAsync(cancellationToken).ConfigureAwait(false);
4242
}
4343

44-
var provisioningResource = new AzureProvisioningResource(resource.Name, context.BuildWebSite)
44+
var provisioningResource = new AzureAppServiceWebSiteResource(resource.Name, context.BuildWebSite, resource)
4545
{
4646
ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions
4747
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
6+
namespace Aspire.Hosting.Azure;
7+
8+
/// <summary>
9+
/// Represents an Azure App Service Web Site resource.
10+
/// </summary>
11+
/// <param name="name">The name of the resource in the Aspire application model.</param>
12+
/// <param name="configureInfrastructure">Callback to configure the Azure resources.</param>
13+
/// <param name="targetResource">The target resource that this Azure Web Site is being created for.</param>
14+
public class AzureAppServiceWebSiteResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure, IResource targetResource)
15+
: AzureProvisioningResource(name, configureInfrastructure)
16+
{
17+
/// <summary>
18+
/// Gets the target resource that this Azure Web Site is being created for.
19+
/// </summary>
20+
public IResource TargetResource { get; } = targetResource;
21+
}

tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ public async Task DeployAsync_WithAzureFunctionsProject_Works()
835835
Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
836836

837837
// Assert that funcapp outputs are propagated
838-
var funcAppDeployment = Assert.IsType<AzureProvisioningResource>(funcApp.Resource.GetDeploymentTargetAnnotation()?.DeploymentTarget);
838+
var funcAppDeployment = Assert.IsAssignableFrom<AzureProvisioningResource>(funcApp.Resource.GetDeploymentTargetAnnotation()?.DeploymentTarget);
839839
Assert.NotNull(funcAppDeployment);
840840
Assert.Equal(await ((BicepOutputReference)funcAppDeployment.Parameters["env_outputs_azure_container_apps_environment_default_domain"]!).GetValueAsync(), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
841841
Assert.Equal(await ((BicepOutputReference)funcAppDeployment.Parameters["env_outputs_azure_container_apps_environment_id"]!).GetValueAsync(), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.CompilerServices;
5+
using Aspire.Hosting.ApplicationModel;
6+
using Aspire.Hosting.Azure.AppContainers;
7+
using Aspire.Hosting.Utils;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
namespace Aspire.Hosting.Azure.Tests;
11+
12+
public class AzureProvisioningResourceTests
13+
{
14+
[Fact]
15+
public async Task PublishAsAzureContainerApp_CreatesAzureContainerAppResource()
16+
{
17+
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
18+
19+
builder.AddAzureContainerAppEnvironment("env");
20+
21+
var apiProject = builder.AddProject<Project>("api", launchProfileName: null);
22+
apiProject.PublishAsAzureContainerApp((infrastructure, containerApp) =>
23+
{
24+
// This callback should have access to the original resource
25+
// via the AzureContainerAppResource.TargetResource property
26+
Assert.IsType<AzureContainerAppResource>(infrastructure.AspireResource);
27+
var containerAppResource = (AzureContainerAppResource)infrastructure.AspireResource;
28+
29+
Assert.Same(apiProject.Resource, containerAppResource.TargetResource);
30+
});
31+
32+
using var app = builder.Build();
33+
34+
await ExecuteBeforeStartHooksAsync(app, default);
35+
36+
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
37+
var project = Assert.Single(model.GetProjectResources());
38+
39+
// Verify the deployment target was created
40+
project.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);
41+
var provisioningResource = target?.DeploymentTarget as AzureContainerAppResource;
42+
Assert.NotNull(provisioningResource);
43+
44+
// Verify the target resource is accessible
45+
Assert.Same(apiProject.Resource, provisioningResource.TargetResource);
46+
}
47+
48+
[Fact]
49+
public async Task PublishAsAzureAppServiceWebsite_CreatesAzureWebSiteResource()
50+
{
51+
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
52+
53+
builder.AddAzureAppServiceEnvironment("env");
54+
55+
var apiProject = builder.AddProject<Project>("api", launchProfileName: null);
56+
apiProject.PublishAsAzureAppServiceWebsite((infrastructure, website) =>
57+
{
58+
// This callback should have access to the original resource
59+
// via the AzureAppServiceWebSiteResource.TargetResource property
60+
Assert.IsType<AzureAppServiceWebSiteResource>(infrastructure.AspireResource);
61+
var webSiteResource = (AzureAppServiceWebSiteResource)infrastructure.AspireResource;
62+
63+
Assert.Same(apiProject.Resource, webSiteResource.TargetResource);
64+
});
65+
66+
using var app = builder.Build();
67+
68+
await ExecuteBeforeStartHooksAsync(app, default);
69+
70+
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
71+
var project = Assert.Single(model.GetProjectResources());
72+
73+
// Verify the deployment target was created
74+
project.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);
75+
var provisioningResource = target?.DeploymentTarget as AzureAppServiceWebSiteResource;
76+
Assert.NotNull(provisioningResource);
77+
78+
// Verify the target resource is accessible
79+
Assert.Same(apiProject.Resource, provisioningResource.TargetResource);
80+
}
81+
82+
[Fact]
83+
public async Task ContainerResource_WithPublishAsContainerApp_CreatesAzureContainerAppResource()
84+
{
85+
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
86+
87+
builder.AddAzureContainerAppEnvironment("env");
88+
89+
var container = builder.AddContainer("api", "myimage");
90+
container.PublishAsAzureContainerApp((infrastructure, containerApp) =>
91+
{
92+
// Verify we can access the original container resource
93+
Assert.IsType<AzureContainerAppResource>(infrastructure.AspireResource);
94+
var containerAppResource = (AzureContainerAppResource)infrastructure.AspireResource;
95+
96+
Assert.Same(container.Resource, containerAppResource.TargetResource);
97+
});
98+
99+
using var app = builder.Build();
100+
101+
await ExecuteBeforeStartHooksAsync(app, default);
102+
103+
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
104+
var containerResource = Assert.Single(model.GetContainerResources());
105+
106+
// Verify the deployment target was created with the correct type
107+
containerResource.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);
108+
var provisioningResource = target?.DeploymentTarget as AzureContainerAppResource;
109+
Assert.NotNull(provisioningResource);
110+
111+
Assert.Same(container.Resource, provisioningResource.TargetResource);
112+
}
113+
114+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
115+
private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
116+
117+
private sealed class Project : IProjectMetadata
118+
{
119+
public string ProjectPath => "project";
120+
}
121+
}

0 commit comments

Comments
 (0)