Skip to content

Commit bd6da41

Browse files
authored
Allow multiple compute environment resources in an app model (#8820)
* Allow multiple compute environment resources in an app model Contributes to #8786 * Add tests * Add IComputeResource and implement it on Project, Container, and Executable. * Tweak tests * PR feedback
1 parent 7cec38b commit bd6da41

15 files changed

+175
-19
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Aspire.Hosting.Azure.AppContainers;
1212
/// <param name="configureInfrastructure">The callback to configure the Azure infrastructure for this resource.</param>
1313
public class AzureContainerAppEnvironmentResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
1414
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
15-
AzureProvisioningResource(name, configureInfrastructure), IAzureContainerAppEnvironment, IAzureContainerRegistry
15+
AzureProvisioningResource(name, configureInfrastructure), IComputeEnvironmentResource, IAzureContainerAppEnvironment, IAzureContainerRegistry
1616
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
1717
{
1818
internal bool UseAzdNamingConvention { get; set; }

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
6868
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
6969
r.Annotations.Add(new DeploymentTargetAnnotation(containerApp)
7070
{
71-
ContainerRegistryInfo = caes.FirstOrDefault()
71+
ContainerRegistryInfo = caes.FirstOrDefault(),
72+
ComputeEnvironment = environment as IComputeEnvironmentResource // will be null if azd
7273
});
7374
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
7475
}

src/Aspire.Hosting.Azure/AzurePublishingContext.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,7 @@ static BicepValue<string> ResolveValue(object val)
219219

220220
foreach (var resource in model.Resources)
221221
{
222-
if (resource.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var targetAnnotation) &&
223-
targetAnnotation.DeploymentTarget is AzureBicepResource br)
222+
if (resource.GetDeploymentTargetAnnotation()?.DeploymentTarget is AzureBicepResource br)
224223
{
225224
var moduleDirectory = outputDirectory.CreateSubdirectory(resource.Name);
226225

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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.Diagnostics.CodeAnalysis;
5+
6+
namespace Aspire.Hosting.ApplicationModel;
7+
8+
[Experimental("ASPIRECOMPUTE001")]
9+
internal sealed class ComputeEnvironmentAnnotation(IComputeEnvironmentResource computeEnvironment) : IResourceAnnotation
10+
{
11+
public IComputeEnvironmentResource ComputeEnvironment { get; } = computeEnvironment;
12+
}

src/Aspire.Hosting/ApplicationModel/ContainerResource.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ namespace Aspire.Hosting.ApplicationModel;
88
/// </summary>
99
/// <param name="name">The name of the resource.</param>
1010
/// <param name="entrypoint">An optional container entrypoint.</param>
11-
public class ContainerResource(string name, string? entrypoint = null) : Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport
11+
public class ContainerResource(string name, string? entrypoint = null)
12+
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport,
13+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
14+
IComputeResource
15+
#pragma warning restore ASPIRECOMPUTE001
1216
{
1317
/// <summary>
1418
/// The container Entrypoint.

src/Aspire.Hosting/ApplicationModel/DeploymentTargetAnnotation.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@ public sealed class DeploymentTargetAnnotation(IResource target) : IResourceAnno
2121
/// </summary>
2222
[Experimental("ASPIRECOMPUTE001")]
2323
public IContainerRegistry? ContainerRegistryInfo { get; set; }
24+
25+
/// <summary>
26+
/// Gets or sets the compute environment resource associated with the deployment target.
27+
/// </summary>
28+
[Experimental("ASPIRECOMPUTE001")]
29+
public IComputeEnvironmentResource? ComputeEnvironment { get; set; }
2430
}

src/Aspire.Hosting/ApplicationModel/ExecutableResource.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ namespace Aspire.Hosting.ApplicationModel;
1313
/// <param name="command">The command to execute.</param>
1414
/// <param name="workingDirectory">The working directory of the executable.</param>
1515
public class ExecutableResource(string name, string command, string workingDirectory)
16-
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport
16+
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport,
17+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
18+
IComputeResource
19+
#pragma warning restore ASPIRECOMPUTE001
1720
{
1821
/// <summary>
1922
/// Gets the command associated with this executable resource.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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.Diagnostics.CodeAnalysis;
5+
6+
namespace Aspire.Hosting.ApplicationModel;
7+
8+
/// <summary>
9+
/// Represents a compute environment resource.
10+
/// </summary>
11+
[Experimental("ASPIRECOMPUTE001")]
12+
public interface IComputeEnvironmentResource : IResource
13+
{
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.Diagnostics.CodeAnalysis;
5+
6+
namespace Aspire.Hosting.ApplicationModel;
7+
8+
/// <summary>
9+
/// Represents a compute resource.
10+
/// </summary>
11+
/// <remarks>
12+
/// A compute resource is a resource that can be hosted/executed on an <see cref="IComputeEnvironmentResource"/>. Examples
13+
/// include projects, containers, and other resources that can be executed on a compute environment.
14+
/// </remarks>
15+
[Experimental("ASPIRECOMPUTE001")]
16+
public interface IComputeResource : IResource
17+
{
18+
}

src/Aspire.Hosting/ApplicationModel/ProjectResource.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ namespace Aspire.Hosting.ApplicationModel;
77
/// A resource that represents a specified .NET project.
88
/// </summary>
99
/// <param name="name">The name of the resource.</param>
10-
public class ProjectResource(string name) : Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithServiceDiscovery, IResourceWithWaitSupport
10+
public class ProjectResource(string name)
11+
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithServiceDiscovery, IResourceWithWaitSupport,
12+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
13+
IComputeResource
14+
#pragma warning restore ASPIRECOMPUTE001
1115
{
1216
// Keep track of the config host for each Kestrel endpoint annotation
1317
internal Dictionary<EndpointAnnotation, string> KestrelEndpointAnnotationHosts { get; } = new();

src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,42 @@ public static int GetReplicaCount(this IResource resource)
571571
}
572572
}
573573

574+
/// <summary>
575+
/// Gets the deployment target for the specified resource, if any. Throws an exception if
576+
/// there are multiple compute environments and a compute environment is not explicitly specified.
577+
/// </summary>
578+
public static DeploymentTargetAnnotation? GetDeploymentTargetAnnotation(this IResource resource)
579+
{
580+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
581+
if (resource.TryGetLastAnnotation<ComputeEnvironmentAnnotation>(out var computeEnvironmentAnnotation))
582+
{
583+
// find the deployment target for the compute environment
584+
return resource.Annotations
585+
.OfType<DeploymentTargetAnnotation>()
586+
.LastOrDefault(a => a.ComputeEnvironment == computeEnvironmentAnnotation.ComputeEnvironment);
587+
}
588+
else
589+
{
590+
DeploymentTargetAnnotation? result = null;
591+
var computeEnvironments = resource.Annotations.OfType<DeploymentTargetAnnotation>();
592+
foreach (var annotation in computeEnvironments)
593+
{
594+
if (result is null)
595+
{
596+
result = annotation;
597+
}
598+
else
599+
{
600+
var computeEnvironmentNames = string.Join(", ", computeEnvironments.Select(a => a.ComputeEnvironment?.Name));
601+
throw new InvalidOperationException($"Resource '{resource.Name}' has multiple compute environments - '{computeEnvironmentNames}'. Please specify a single compute environment using 'WithComputeEnvironment'.");
602+
}
603+
}
604+
605+
return result;
606+
}
607+
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
608+
}
609+
574610
/// <summary>
575611
/// Gets the lifetime type of the container for the specified resource.
576612
/// Defaults to <see cref="ContainerLifetime.Session"/> if no <see cref="ContainerLifetimeAnnotation"/> is found.

src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ private async Task WriteProjectAsync(ProjectResource project)
155155

156156
var relativePathToProjectFile = GetManifestRelativePath(metadata.ProjectPath);
157157

158-
if (project.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var deploymentTarget))
158+
var deploymentTarget = project.GetDeploymentTargetAnnotation();
159+
if (deploymentTarget is not null)
159160
{
160161
Writer.WriteString("type", "project.v1");
161162
}
@@ -250,7 +251,7 @@ internal Task WriteParameterAsync(ParameterResource parameter)
250251
/// <exception cref="DistributedApplicationException">Thrown if the container resource does not contain a <see cref="ContainerImageAnnotation"/>.</exception>
251252
public async Task WriteContainerAsync(ContainerResource container)
252253
{
253-
container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var deploymentTarget);
254+
var deploymentTarget = container.GetDeploymentTargetAnnotation();
254255

255256
if (container.Annotations.OfType<DockerfileBuildAnnotation>().Any())
256257
{

src/Aspire.Hosting/ResourceBuilderExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Net.Sockets;
56
using Aspire.Dashboard.Model;
67
using Aspire.Hosting.ApplicationModel;
@@ -1933,4 +1934,24 @@ public static IResourceBuilder<T> WithParentRelationship<T>(
19331934
{
19341935
return builder.WithRelationship(parent, KnownRelationshipTypes.Parent);
19351936
}
1937+
1938+
/// <summary>
1939+
/// Configures the compute environment for the compute resource.
1940+
/// </summary>
1941+
/// <param name="builder">The compute resource builder.</param>
1942+
/// <param name="computeEnvironmentResource">The compute environment resource to associate with the compute resource.</param>
1943+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
1944+
/// <remarks>
1945+
/// This method allows associating a specific compute environment with the compute resource.
1946+
/// </remarks>
1947+
[Experimental("ASPIRECOMPUTE001")]
1948+
public static IResourceBuilder<T> WithComputeEnvironment<T>(this IResourceBuilder<T> builder, IResourceBuilder<IComputeEnvironmentResource> computeEnvironmentResource)
1949+
where T : IComputeResource
1950+
{
1951+
ArgumentNullException.ThrowIfNull(builder);
1952+
ArgumentNullException.ThrowIfNull(computeEnvironmentResource);
1953+
1954+
builder.WithAnnotation(new ComputeEnvironmentAnnotation(computeEnvironmentResource.Resource));
1955+
return builder;
1956+
}
19361957
}

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
#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.
5+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
56

67
using System.Text.Json.Nodes;
78
using Aspire.Hosting.ApplicationModel;
@@ -201,11 +202,11 @@ param api_containerimage string
201202
}
202203

203204
[Fact]
204-
public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContainerAppToProjectResources()
205+
public async Task AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources()
205206
{
206207
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
207208

208-
builder.AddAzureContainerAppEnvironment("env");
209+
var env = builder.AddAzureContainerAppEnvironment("env");
209210

210211
builder.AddProject<Project>("api", launchProfileName: null)
211212
.WithHttpEndpoint();
@@ -218,10 +219,11 @@ public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContaine
218219

219220
var container = Assert.Single(model.GetProjectResources());
220221

221-
container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);
222+
var target = container.GetDeploymentTargetAnnotation();
222223

223-
var resource = target?.DeploymentTarget as AzureProvisioningResource;
224+
Assert.Same(env.Resource, target?.ComputeEnvironment);
224225

226+
var resource = target?.DeploymentTarget as AzureProvisioningResource;
225227
Assert.NotNull(resource);
226228

227229
var (manifest, bicep) = await GetManifestWithBicep(resource);
@@ -333,7 +335,7 @@ public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastruc
333335
{
334336
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
335337

336-
builder.AddAzureContainerAppEnvironment("infra");
338+
var infra = builder.AddAzureContainerAppEnvironment("infra");
337339

338340
var env = builder.AddParameter("env");
339341

@@ -356,10 +358,11 @@ public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastruc
356358

357359
var container = Assert.Single(model.GetContainerResources());
358360

359-
container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);
361+
var target = container.GetDeploymentTargetAnnotation();
360362

361-
var resource = target?.DeploymentTarget as AzureProvisioningResource;
363+
Assert.Same(infra.Resource, target?.ComputeEnvironment);
362364

365+
var resource = target?.DeploymentTarget as AzureProvisioningResource;
363366
Assert.NotNull(resource);
364367

365368
var (manifest, bicep) = await GetManifestWithBicep(resource);
@@ -450,7 +453,7 @@ public async Task CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExe
450453
{
451454
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
452455

453-
builder.AddAzureContainerAppEnvironment("env");
456+
var env = builder.AddAzureContainerAppEnvironment("env");
454457

455458
builder.AddExecutable("api", "node.exe", Environment.CurrentDirectory)
456459
.PublishAsDockerFile();
@@ -463,10 +466,11 @@ public async Task CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExe
463466

464467
var container = Assert.Single(model.GetContainerResources());
465468

466-
container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);
469+
var target = container.GetDeploymentTargetAnnotation();
467470

468-
var resource = target?.DeploymentTarget as AzureProvisioningResource;
471+
Assert.Same(env.Resource, target?.ComputeEnvironment);
469472

473+
var resource = target?.DeploymentTarget as AzureProvisioningResource;
470474
Assert.NotNull(resource);
471475

472476
var (manifest, bicep) = await GetManifestWithBicep(resource);

tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
5+
46
using Aspire.Hosting.Utils;
57
using Microsoft.AspNetCore.InternalTesting;
68
using Xunit;
@@ -290,6 +292,37 @@ public async Task GetArgumentValuesAsync_ReturnsCorrectValuesForSpecialCases()
290292
Assert.Equal<IEnumerable<string>>(["ConnectionString", "SecretParameter", "NonSecretParameter"], executableArgs);
291293
}
292294

295+
[Fact]
296+
public void GetDeploymentTargetAnnotationWorks()
297+
{
298+
var builder = DistributedApplication.CreateBuilder();
299+
300+
var compute1 = builder.AddResource(new ComputeEnvironmentResource("compute1"));
301+
var compute2 = builder.AddResource(new ComputeEnvironmentResource("compute2"));
302+
303+
void RunTest<T>(IResourceBuilder<T> resourceBuilder) where T : IComputeResource
304+
{
305+
resourceBuilder
306+
.WithAnnotation(new DeploymentTargetAnnotation(compute1.Resource) { ComputeEnvironment = compute1.Resource })
307+
.WithAnnotation(new DeploymentTargetAnnotation(compute2.Resource) { ComputeEnvironment = compute2.Resource });
308+
309+
var ex = Assert.Throws<InvalidOperationException>(resourceBuilder.Resource.GetDeploymentTargetAnnotation);
310+
Assert.Contains("'compute1, compute2'", ex.Message);
311+
312+
resourceBuilder.WithComputeEnvironment(compute2);
313+
314+
Assert.Equal(compute2.Resource, resourceBuilder.Resource.GetDeploymentTargetAnnotation()!.ComputeEnvironment);
315+
}
316+
317+
RunTest(builder.AddContainer("myContainer", "nginx"));
318+
RunTest(builder.AddProject<Projects.ServiceA>("ServiceA"));
319+
RunTest(builder.AddExecutable("myExecutable", "nginx", string.Empty));
320+
}
321+
322+
private sealed class ComputeEnvironmentResource(string name) : Resource(name), IComputeEnvironmentResource
323+
{
324+
}
325+
293326
private sealed class ParentResource(string name) : Resource(name)
294327
{
295328

0 commit comments

Comments
 (0)