diff --git a/src/Aspire.Hosting/ApplicationModel/ContainerLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ContainerLifetimeAnnotation.cs new file mode 100644 index 00000000000..5cc5bfc0002 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/ContainerLifetimeAnnotation.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Lifetime modes for container resources +/// +public enum ContainerLifetimeType +{ + /// + /// The default lifetime behavior should apply. This will create the resource when the AppHost starts and dispose of it when the AppHost shuts down. + /// + Default, + /// + /// The resource is persistent and will not be disposed of when the AppHost shuts down. + /// + Persistent, +} + +/// +/// 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) +/// +[DebuggerDisplay("Type = {GetType().Name,nq}")] +public sealed class ContainerLifetimeAnnotation : IResourceAnnotation +{ + /// + /// Gets or sets the lifetime type for the container resource. + /// + public required ContainerLifetimeType LifetimeType { get; set; } +} \ No newline at end of file diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 6f0f92d18e1..9801d6b1b5f 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -84,9 +84,9 @@ public static bool TryGetEnvironmentVariables(this IResource resource, [NotNullW /// var container = builder.AddContainer("elasticsearch", "library/elasticsearch", "8.14.0") /// .WithEnvironment("discovery.type", "single-node") /// .WithEnvironment("xpack.security.enabled", "true"); - /// + /// /// var env = await container.Resource.GetEnvironmentVariableValuesAsync(); - /// + /// /// Assert.Collection(env, /// env => /// { @@ -179,7 +179,7 @@ public static IEnumerable GetEndpoints(this IResourceWithEndp /// /// The which contains annotations. /// The name of the endpoint. - /// An object representing the endpoint reference + /// An object representing the endpoint reference /// for the specified endpoint. public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName) { @@ -233,4 +233,20 @@ public static int GetReplicaCount(this IResource resource) return 1; } } + + /// + /// Gets the lifetime type of the container for the specified resoruce. Defaults to if + /// no is found. + /// + /// The resource to the get the ContainerLifetimeType for. + /// The from the for the resource (if the annotation exists). Defaults to if the annotation is not set. + internal static ContainerLifetimeType GetContainerLifetimeType(this IResource resource) + { + if (resource.TryGetLastAnnotation(out var lifetimeAnnotation)) + { + return lifetimeAnnotation.LifetimeType; + } + + return ContainerLifetimeType.Default; + } } diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 777a757e974..5f6dc2412c0 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; @@ -221,6 +222,27 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil return builder.WithAnnotation(annotation); } + /// + /// Sets the lifetime behavior of the container resource. + /// + /// The resource type. + /// Builder for the container resource. + /// The lifetime behavior of the container resource (defaults behavior is ) + /// The . + /// + /// Marking a container resource to have a lifetime. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// builder.AddContainer("mycontainer", "myimage") + /// .WithContainerLifetime(ContainerLifetimeType.Persistent); + /// + /// + [Experimental("ASPIRECONTAINERLIFETIME001")] + public static IResourceBuilder WithContainerLifetime(this IResourceBuilder builder, ContainerLifetimeType lifetimeType) where T : ContainerResource + { + return builder.WithAnnotation(new ContainerLifetimeAnnotation { LifetimeType = lifetimeType }, ResourceAnnotationMutationBehavior.Replace); + } + private static IResourceBuilder ThrowResourceIsNotContainer(IResourceBuilder builder) where T : ContainerResource { throw new InvalidOperationException($"The resource '{builder.Resource.Name}' does not have a container image specified. Use WithImage to specify the container image and tag."); diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index e29d5ff2dea..465d06d5c94 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -1327,11 +1327,23 @@ private void PrepareContainers() throw new InvalidOperationException(); } - var nameSuffix = GetRandomNameSuffix(); + var nameSuffix = string.Empty; + + if (container.GetContainerLifetimeType() == ContainerLifetimeType.Default) + { + nameSuffix = GetRandomNameSuffix(); + } + var containerObjectName = GetObjectNameForResource(container, nameSuffix); var ctr = Container.Create(containerObjectName, containerImageName); ctr.Spec.ContainerName = containerObjectName; // Use the same name for container orchestrator (Docker, Podman) resource and DCP object name. + + if (container.GetContainerLifetimeType() == ContainerLifetimeType.Persistent) + { + ctr.Spec.Persistent = true; + } + ctr.Annotate(CustomResource.ResourceNameAnnotation, container.Name); ctr.Annotate(CustomResource.OtelServiceNameAnnotation, container.Name); ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, nameSuffix); diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt index a375d4c358b..111fa6cf9ff 100644 --- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt @@ -11,6 +11,13 @@ Aspire.Hosting.ApplicationModel.BeforeStartEvent Aspire.Hosting.ApplicationModel.BeforeStartEvent.BeforeStartEvent(System.IServiceProvider! services, Aspire.Hosting.ApplicationModel.DistributedApplicationModel! model) -> void Aspire.Hosting.ApplicationModel.BeforeStartEvent.Model.get -> Aspire.Hosting.ApplicationModel.DistributedApplicationModel! Aspire.Hosting.ApplicationModel.BeforeStartEvent.Services.get -> System.IServiceProvider! +Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation +Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.ContainerLifetimeAnnotation() -> void +Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.LifetimeType.get -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType +Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.LifetimeType.set -> void +Aspire.Hosting.ApplicationModel.ContainerLifetimeType +Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Default = 0 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType +Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Persistent = 1 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType Aspire.Hosting.ApplicationModel.ResourceNotificationService.ResourceNotificationService(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Hosting.IHostApplicationLifetime! hostApplicationLifetime) -> void Aspire.Hosting.DistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eventing.IDistributedApplicationEventing! Aspire.Hosting.Eventing.DistributedApplicationEventing @@ -37,6 +44,7 @@ Aspire.Hosting.IDistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eve static Aspire.Hosting.ApplicationModel.ResourceExtensions.GetEnvironmentVariableValuesAsync(this Aspire.Hosting.ApplicationModel.IResourceWithEnvironment! resource, Aspire.Hosting.DistributedApplicationOperation applicationOperation = Aspire.Hosting.DistributedApplicationOperation.Run) -> System.Threading.Tasks.ValueTask!> Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, string? targetState = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Collections.Generic.IEnumerable! targetStates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerLifetime(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.ContainerLifetimeType lifetimeType) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ProjectResourceBuilderExtensions.WithEndpointsInEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Func! filter) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! Aspire.Hosting.DistributedApplicationExecutionContext.DistributedApplicationExecutionContext(Aspire.Hosting.DistributedApplicationExecutionContextOptions! options) -> void Aspire.Hosting.DistributedApplicationExecutionContext.ServiceProvider.get -> System.IServiceProvider!