File: ApplicationModel\Docker\ContainerFilesExtensions.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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 Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.ApplicationModel.Docker;
 
/// <summary>
/// Provides Dockerfile builder extension methods for supporting <see cref="ResourceBuilderExtensions.PublishWithContainerFiles" />.
/// </summary>
public static class ContainerFilesExtensions
{
    /// <summary>
    /// Adds Dockerfile instructions to include container files from the specified resource into the Dockerfile build
    /// process.
    /// </summary>
    /// <param name="builder">The Dockerfile builder to which container file instructions will be added. Cannot be null.</param>
    /// <param name="resource">The resource containing container files to be added to the Dockerfile. Cannot be null.</param>
    /// <param name="logger">An optional logger used to record warnings if container image names cannot be determined for source resources.</param>
    /// <returns>The same DockerfileBuilder instance with additional instructions for container files, enabling method chaining.</returns>
    [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
    public static DockerfileBuilder AddContainerFilesStages(this DockerfileBuilder builder, IResource resource, ILogger? logger)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(resource);
 
        if (resource.TryGetAnnotationsOfType<ContainerFilesDestinationAnnotation>(out var containerFilesDestinationAnnotations))
        {
            foreach (var containerFileDestination in containerFilesDestinationAnnotations)
            {
                var source = containerFileDestination.Source;
 
                // get image name - skip this source if it doesn't have an image name
                if (!source.TryGetContainerImageName(out var sourceImageName))
                {
                    logger?.LogWarning("Cannot get container image name for source resource {SourceName}, skipping", source.Name);
                    continue;
                }
 
                var sourceImageArgName = GetSourceImageArgName(source);
                builder.Arg(sourceImageArgName, sourceImageName);
 
                var sourceImageStageName = GetSourceStageName(source);
                builder.From("${" + sourceImageArgName + "}", sourceImageStageName);
            }
        }
        return builder;
    }
 
    /// <summary>
    /// Adds COPY --from statements to the Dockerfile stage for container files from resources referenced by <see cref="ContainerFilesDestinationAnnotation"/>.
    /// </summary>
    /// <param name="stage">The Dockerfile stage to add container file copy statements to.</param>
    /// <param name="resource">The resource that may have <see cref="ContainerFilesDestinationAnnotation"/> annotations specifying files to copy.</param>
    /// <param name="rootDestinationPath">The root destination path in the container. Relative paths in annotations will be appended to this path.</param>
    /// <param name="logger">The logger used for logging information or errors.</param>
    /// <returns>The <see cref="DockerfileStage"/> to allow for fluent chaining.</returns>
    /// <remarks>
    /// <para>
    /// This method processes all <see cref="ContainerFilesDestinationAnnotation"/> annotations on the resource
    /// and generates COPY --from statements for each source container's files.
    /// </para>
    /// <para>
    /// For each annotation:
    /// <list type="bullet">
    /// <item>If the source resource has a container image name (via <c>TryGetContainerImageName</c>), COPY statements are generated</item>
    /// <item>If the source resource does not have a container image name, it is skipped</item>
    /// <item>Relative destination paths are combined with <paramref name="rootDestinationPath"/></item>
    /// <item>Absolute destination paths are used as-is</item>
    /// <item>Each <see cref="ContainerFilesSourceAnnotation"/> on the source resource generates a COPY statement</item>
    /// </list>
    /// </para>
    /// <para>
    /// This is typically used when building container images that need to include files from other containers,
    /// such as copying static assets from a frontend build container into a backend API container.
    /// </para>
    /// </remarks>
    [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
    public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IResource resource, string rootDestinationPath, ILogger? logger)
    {
        ArgumentNullException.ThrowIfNull(stage);
        ArgumentNullException.ThrowIfNull(resource);
        ArgumentNullException.ThrowIfNull(rootDestinationPath);
 
        if (resource.TryGetAnnotationsOfType<ContainerFilesDestinationAnnotation>(out var containerFilesDestinationAnnotations))
        {
            foreach (var containerFileDestination in containerFilesDestinationAnnotations)
            {
                var source = containerFileDestination.Source;
 
                // get image name - skip this source if it doesn't have an image name
                if (!source.TryGetContainerImageName(out var _))
                {
                    logger?.LogWarning("Cannot get container image name for source resource {SourceName}, skipping", source.Name);
                    continue;
                }
 
                var sourceImageStageName = GetSourceStageName(source);
 
                var destinationPath = containerFileDestination.DestinationPath;
                if (!destinationPath.StartsWith('/'))
                {
                    destinationPath = $"{rootDestinationPath}/{destinationPath}";
                }
 
                foreach (var containerFilesSource in source.Annotations.OfType<ContainerFilesSourceAnnotation>())
                {
                    logger?.LogDebug("Adding COPY --from={SourceImage} {SourcePath} {DestinationPath}",
                        sourceImageStageName, containerFilesSource.SourcePath, destinationPath);
                    stage.CopyFrom(sourceImageStageName, containerFilesSource.SourcePath, destinationPath);
                }
            }
 
            stage.EmptyLine();
        }
        return stage;
    }
 
    private static string GetSourceImageArgName(IResource source) => $"{source.Name.ToUpperInvariant()}_IMAGENAME";
 
    private static string GetSourceStageName(IResource source) => $"{source.Name}_stage";
}