File: ExecutableResourceBuilderExtensions.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 Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Provides extension methods for adding executable resources to the <see cref="IDistributedApplicationBuilder"/> application model.
/// </summary>
public static class ExecutableResourceBuilderExtensions
{
    /// <summary>
    /// Adds an executable resource to the application model.
    /// </summary>
    /// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
    /// <param name="name">The name of the resource.</param>
    /// <param name="command">The executable path. This can be a fully qualified path or a executable to run from the shell/command line.</param>
    /// <param name="workingDirectory">The working directory of the executable.</param>
    /// <param name="args">The arguments to the executable.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
    public static IResourceBuilder<ExecutableResource> AddExecutable(this IDistributedApplicationBuilder builder, [ResourceName] string name, string command, string workingDirectory, params string[]? args)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(name);
        ArgumentNullException.ThrowIfNull(command);
        ArgumentNullException.ThrowIfNull(workingDirectory);
 
        return AddExecutable(builder, name, command, workingDirectory, (object[]?)args);
    }
 
    /// <summary>
    /// Adds an executable resource to the application model.
    /// </summary>
    /// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
    /// <param name="name">The name of the resource.</param>
    /// <param name="command">The executable path. This can be a fully qualified path or a executable to run from the shell/command line.</param>
    /// <param name="workingDirectory">The working directory of the executable.</param>
    /// <param name="args">The arguments to the executable.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
    public static IResourceBuilder<ExecutableResource> AddExecutable(this IDistributedApplicationBuilder builder, [ResourceName] string name, string command, string workingDirectory, params object[]? args)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(name);
        ArgumentNullException.ThrowIfNull(command);
        ArgumentNullException.ThrowIfNull(workingDirectory);
 
        workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory));
 
        var executable = new ExecutableResource(name, command, workingDirectory);
        return builder.AddResource(executable)
                      .WithArgs(context =>
                      {
                          if (args is not null)
                          {
                              context.Args.AddRange(args);
                          }
                      });
    }
 
    /// <summary>
    /// Adds annotation to <see cref="ExecutableResource" /> to support containerization during deployment.
    /// </summary>
    /// <typeparam name="T">Type of executable resource</typeparam>
    /// <param name="builder">Resource builder</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    public static IResourceBuilder<T> PublishAsDockerFile<T>(this IResourceBuilder<T> builder) where T : ExecutableResource
    {
        return builder.PublishAsDockerFile(c => { });
    }
 
    /// <summary>
    /// Adds annotation to <see cref="ExecutableResource" /> to support containerization during deployment.
    /// The resulting container image is built, and when the optional <paramref name="buildArgs"/> are provided
    /// they're used with <c>docker build --build-arg</c>.
    /// </summary>
    /// <typeparam name="T">Type of executable resource</typeparam>
    /// <param name="builder">Resource builder</param>
    /// <param name="buildArgs">The optional build arguments, used with <c>docker build --build-args</c>.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    public static IResourceBuilder<T> PublishAsDockerFile<T>(this IResourceBuilder<T> builder, IEnumerable<DockerBuildArg>? buildArgs) where T : ExecutableResource
    {
        ArgumentNullException.ThrowIfNull(builder);
 
        return builder.PublishAsDockerFile(c =>
        {
            foreach (var arg in buildArgs ?? [])
            {
                c.WithBuildArg(arg.Name, arg.Value);
            }
        });
    }
 
    /// <summary>
    /// Adds support for containerizing this <see cref="ExecutableResource"/> during deployment.
    /// The resulting container image is built, and when the optional <paramref name="configure"/> action is provided,
    /// it is used to configure the container resource.
    /// </summary>
    /// <remarks>
    /// When the executable resource is converted to a container resource, the arguments to the executable
    /// are not used. This is because arguments to the executable often contain physical paths that are not valid
    /// in the container. The container can be set up with the correct arguments using the <paramref name="configure"/> action.
    /// </remarks>
    /// <typeparam name="T">Type of executable resource</typeparam>
    /// <param name="builder">Resource builder</param>
    /// <param name="configure">Optional action to configure the container resource</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    public static IResourceBuilder<T> PublishAsDockerFile<T>(this IResourceBuilder<T> builder, Action<IResourceBuilder<ContainerResource>>? configure)
        where T : ExecutableResource
    {
        if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
        {
            return builder;
        }
 
        // The implementation here is less than ideal, but we don't have a clean way of building resource types
        // that change their behavior based on the context. In this case, we want to change the behavior of the
        // resource from an ExecutableResource to a ContainerResource. We do this by removing the ExecutableResource
        // from the application model and adding a new ContainerResource in its place in publish mode.
 
        // There are still dangling references to the original ExecutableResource in the application model, but
        // in publish mode, it won't be used. This is a limitation of the current design.
        builder.ApplicationBuilder.Resources.Remove(builder.Resource);
 
        var container = new ExecutableContainerResource(builder.Resource);
        var cb = builder.ApplicationBuilder.AddResource(container);
        // WithImage makes this a container resource (adding the annotation)
        cb.WithImage(builder.Resource.Name);
        cb.WithDockerfile(contextPath: builder.Resource.WorkingDirectory);
        // Arguments to the executable often contain physical paths that are not valid in the container
        // Clear them out so that the container can be set up with the correct arguments
        cb.WithArgs(c => c.Args.Clear());
 
        configure?.Invoke(cb);
 
        // Even through we're adding a ContainerResource
        // update the manifest publishing callback on the original ExecutableResource
        // so that the container resource is written to the manifest
        return builder.WithManifestPublishingCallback(context =>
            context.WriteContainerAsync(container));
    }
 
    // Allows us to mirror annotations from ExecutableResource to ContainerResource
    private sealed class ExecutableContainerResource(ExecutableResource er) : ContainerResource(er.Name)
    {
        public override ResourceAnnotationCollection Annotations => er.Annotations;
    }
}