Skip to content

Commit 23753e1

Browse files
authored
Persistent container support (#5354)
* Proof of concept of persistent container * Update API * Update check to internal and mark API as experimental * Add example for WithContainerLifetime * Add missing returns doc * Change AppHost lifetime to Default
1 parent 52bc399 commit 23753e1

File tree

5 files changed

+95
-4
lines changed

5 files changed

+95
-4
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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;
5+
6+
namespace Aspire.Hosting.ApplicationModel;
7+
8+
/// <summary>
9+
/// Lifetime modes for container resources
10+
/// </summary>
11+
public enum ContainerLifetimeType
12+
{
13+
/// <summary>
14+
/// The default lifetime behavior should apply. This will create the resource when the AppHost starts and dispose of it when the AppHost shuts down.
15+
/// </summary>
16+
Default,
17+
/// <summary>
18+
/// The resource is persistent and will not be disposed of when the AppHost shuts down.
19+
/// </summary>
20+
Persistent,
21+
}
22+
23+
/// <summary>
24+
/// Annotation that controls the lifetime of a container resource (default behavior that matches the lifetime of the AppHost or a persistent lifetime across AppHost restarts)
25+
/// </summary>
26+
[DebuggerDisplay("Type = {GetType().Name,nq}")]
27+
public sealed class ContainerLifetimeAnnotation : IResourceAnnotation
28+
{
29+
/// <summary>
30+
/// Gets or sets the lifetime type for the container resource.
31+
/// </summary>
32+
public required ContainerLifetimeType LifetimeType { get; set; }
33+
}

src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ public static bool TryGetEnvironmentVariables(this IResource resource, [NotNullW
8484
/// var container = builder.AddContainer("elasticsearch", "library/elasticsearch", "8.14.0")
8585
/// .WithEnvironment("discovery.type", "single-node")
8686
/// .WithEnvironment("xpack.security.enabled", "true");
87-
///
87+
///
8888
/// var env = await container.Resource.GetEnvironmentVariableValuesAsync();
89-
///
89+
///
9090
/// Assert.Collection(env,
9191
/// env =>
9292
/// {
@@ -179,7 +179,7 @@ public static IEnumerable<EndpointReference> GetEndpoints(this IResourceWithEndp
179179
/// </summary>
180180
/// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
181181
/// <param name="endpointName">The name of the endpoint.</param>
182-
/// <returns>An <see cref="EndpointReference"/> object representing the endpoint reference
182+
/// <returns>An <see cref="EndpointReference"/> object representing the endpoint reference
183183
/// for the specified endpoint.</returns>
184184
public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName)
185185
{
@@ -233,4 +233,20 @@ public static int GetReplicaCount(this IResource resource)
233233
return 1;
234234
}
235235
}
236+
237+
/// <summary>
238+
/// Gets the lifetime type of the container for the specified resoruce. Defaults to <see cref="ContainerLifetimeType.Default"/> if
239+
/// no <see cref="ContainerLifetimeAnnotation"/> is found.
240+
/// </summary>
241+
/// <param name="resource">The resource to the get the ContainerLifetimeType for.</param>
242+
/// <returns>The <see cref="ContainerLifetimeType"/> from the <see cref="ContainerLifetimeAnnotation"/> for the resource (if the annotation exists). Defaults to <see cref="ContainerLifetimeType.Default"/> if the annotation is not set.</returns>
243+
internal static ContainerLifetimeType GetContainerLifetimeType(this IResource resource)
244+
{
245+
if (resource.TryGetLastAnnotation<ContainerLifetimeAnnotation>(out var lifetimeAnnotation))
246+
{
247+
return lifetimeAnnotation.LifetimeType;
248+
}
249+
250+
return ContainerLifetimeType.Default;
251+
}
236252
}

src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs

Lines changed: 22 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 Aspire.Hosting.ApplicationModel;
56
using Aspire.Hosting.Utils;
67

@@ -221,6 +222,27 @@ public static IResourceBuilder<T> WithContainerRuntimeArgs<T>(this IResourceBuil
221222
return builder.WithAnnotation(annotation);
222223
}
223224

225+
/// <summary>
226+
/// Sets the lifetime behavior of the container resource.
227+
/// </summary>
228+
/// <typeparam name="T">The resource type.</typeparam>
229+
/// <param name="builder">Builder for the container resource.</param>
230+
/// <param name="lifetimeType">The lifetime behavior of the container resource (defaults behavior is <see cref="ContainerLifetimeType.Default"/>)</param>
231+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
232+
/// <example>
233+
/// Marking a container resource to have a <see cref="ContainerLifetimeType.Persistent"/> lifetime.
234+
/// <code language="csharp">
235+
/// var builder = DistributedApplication.CreateBuilder(args);
236+
/// builder.AddContainer("mycontainer", "myimage")
237+
/// .WithContainerLifetime(ContainerLifetimeType.Persistent);
238+
/// </code>
239+
/// </example>
240+
[Experimental("ASPIRECONTAINERLIFETIME001")]
241+
public static IResourceBuilder<T> WithContainerLifetime<T>(this IResourceBuilder<T> builder, ContainerLifetimeType lifetimeType) where T : ContainerResource
242+
{
243+
return builder.WithAnnotation(new ContainerLifetimeAnnotation { LifetimeType = lifetimeType }, ResourceAnnotationMutationBehavior.Replace);
244+
}
245+
224246
private static IResourceBuilder<T> ThrowResourceIsNotContainer<T>(IResourceBuilder<T> builder) where T : ContainerResource
225247
{
226248
throw new InvalidOperationException($"The resource '{builder.Resource.Name}' does not have a container image specified. Use WithImage to specify the container image and tag.");

src/Aspire.Hosting/Dcp/ApplicationExecutor.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1331,11 +1331,23 @@ private void PrepareContainers()
13311331
throw new InvalidOperationException();
13321332
}
13331333

1334-
var nameSuffix = GetRandomNameSuffix();
1334+
var nameSuffix = string.Empty;
1335+
1336+
if (container.GetContainerLifetimeType() == ContainerLifetimeType.Default)
1337+
{
1338+
nameSuffix = GetRandomNameSuffix();
1339+
}
1340+
13351341
var containerObjectName = GetObjectNameForResource(container, nameSuffix);
13361342
var ctr = Container.Create(containerObjectName, containerImageName);
13371343

13381344
ctr.Spec.ContainerName = containerObjectName; // Use the same name for container orchestrator (Docker, Podman) resource and DCP object name.
1345+
1346+
if (container.GetContainerLifetimeType() == ContainerLifetimeType.Persistent)
1347+
{
1348+
ctr.Spec.Persistent = true;
1349+
}
1350+
13391351
ctr.Annotate(CustomResource.ResourceNameAnnotation, container.Name);
13401352
ctr.Annotate(CustomResource.OtelServiceNameAnnotation, container.Name);
13411353
ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, nameSuffix);

src/Aspire.Hosting/PublicAPI.Unshipped.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ Aspire.Hosting.ApplicationModel.BeforeStartEvent
1515
Aspire.Hosting.ApplicationModel.BeforeStartEvent.BeforeStartEvent(System.IServiceProvider! services, Aspire.Hosting.ApplicationModel.DistributedApplicationModel! model) -> void
1616
Aspire.Hosting.ApplicationModel.BeforeStartEvent.Model.get -> Aspire.Hosting.ApplicationModel.DistributedApplicationModel!
1717
Aspire.Hosting.ApplicationModel.BeforeStartEvent.Services.get -> System.IServiceProvider!
18+
Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation
19+
Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.ContainerLifetimeAnnotation() -> void
20+
Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.LifetimeType.get -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
21+
Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.LifetimeType.set -> void
22+
Aspire.Hosting.ApplicationModel.ContainerLifetimeType
23+
Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Default = 0 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
24+
Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Persistent = 1 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
1825
Aspire.Hosting.ApplicationModel.ResourceNotificationService.ResourceNotificationService(Microsoft.Extensions.Logging.ILogger<Aspire.Hosting.ApplicationModel.ResourceNotificationService!>! logger, Microsoft.Extensions.Hosting.IHostApplicationLifetime! hostApplicationLifetime) -> void
1926
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Func<Aspire.Hosting.ApplicationModel.ResourceEvent!, bool>! predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Aspire.Hosting.ApplicationModel.ResourceEvent!>!
2027
Aspire.Hosting.DistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eventing.IDistributedApplicationEventing!
@@ -42,6 +49,7 @@ Aspire.Hosting.IDistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eve
4249
static Aspire.Hosting.ApplicationModel.ResourceExtensions.GetEnvironmentVariableValuesAsync(this Aspire.Hosting.ApplicationModel.IResourceWithEnvironment! resource, Aspire.Hosting.DistributedApplicationOperation applicationOperation = Aspire.Hosting.DistributedApplicationOperation.Run) -> System.Threading.Tasks.ValueTask<System.Collections.Generic.Dictionary<string!, string!>!>
4350
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, string? targetState = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
4451
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Collections.Generic.IEnumerable<string!>! targetStates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<string!>!
52+
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerLifetime<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, Aspire.Hosting.ApplicationModel.ContainerLifetimeType lifetimeType) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
4553
static Aspire.Hosting.ProjectResourceBuilderExtensions.WithEndpointsInEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>! builder, System.Func<Aspire.Hosting.ApplicationModel.EndpointAnnotation!, bool>! filter) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>!
4654
Aspire.Hosting.DistributedApplicationExecutionContext.DistributedApplicationExecutionContext(Aspire.Hosting.DistributedApplicationExecutionContextOptions! options) -> void
4755
Aspire.Hosting.DistributedApplicationExecutionContext.ServiceProvider.get -> System.IServiceProvider!

0 commit comments

Comments
 (0)