File: Pipelines\PipelineStepHelpers.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.
 
#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIRECONTAINERRUNTIME001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Pipelines;
 
/// <summary>
/// Helper methods for common pipeline step operations.
/// </summary>
internal static class PipelineStepHelpers
{
    /// <summary>
    /// Pushes a container image to a registry with proper logging and task reporting.
    /// If the registry is a local registry (empty endpoint), only tags the image without pushing.
    /// If the image format is OCI (or other non-Docker format), this is a no-op since OCI images
    /// are typically written to local file paths and don't require a push to a registry.
    /// </summary>
    /// <param name="resource">The resource whose image should be pushed.</param>
    /// <param name="context">The pipeline step context for logging and task reporting.</param>
    /// <returns>A task representing the async operation.</returns>
    public static async Task PushImageToRegistryAsync(IResource resource, PipelineStepContext context)
    {
        // Check if the resource is configured to save images as archives
        // rather than pushing to a registry. These don't require a push operation.
        var buildOptionsContext = await resource.ProcessContainerBuildOptionsCallbackAsync(
            context.Services,
            context.Logger,
            context.ExecutionContext,
            context.CancellationToken).ConfigureAwait(false);
 
        // Skip push operation if Destination is explicitly set to Archive
        if (buildOptionsContext.Destination == ContainerImageDestination.Archive)
        {
            context.Logger.LogInformation("Skipping push for resource '{ResourceName}' - destination is {Destination}, not Registry", 
                resource.Name, buildOptionsContext.Destination);
            return;
        }
 
        var registry = resource.GetContainerRegistry();
        var registryEndpoint = await registry.Endpoint.GetValueAsync(context.CancellationToken).ConfigureAwait(false);
 
        // Check if this is a local registry (empty endpoint means no remote push needed)
        var isLocalRegistry = string.IsNullOrEmpty(registryEndpoint);
 
        if (isLocalRegistry)
        {
            await TagImageForLocalRegistryAsync(resource, context).ConfigureAwait(false);
        }
        else
        {
            await PushImageToRemoteRegistryAsync(resource, registry, context).ConfigureAwait(false);
        }
    }
 
    private static async Task TagImageForLocalRegistryAsync(IResource resource, PipelineStepContext context)
    {
        IValueProvider cir = new ContainerImageReference(resource);
        var targetTag = await cir.GetValueAsync(new ValueProviderContext { ExecutionContext = context.ExecutionContext }, context.CancellationToken).ConfigureAwait(false);
 
        var tagTask = await context.ReportingStep.CreateTaskAsync(
            $"Tagging **{resource.Name}** for local use",
            context.CancellationToken).ConfigureAwait(false);
 
        await using (tagTask.ConfigureAwait(false))
        {
            try
            {
                if (targetTag is null)
                {
                    throw new InvalidOperationException($"Failed to get target tag for {resource.Name}");
                }
 
                // Get the local image name
                var localImageName = resource.TryGetContainerImageName(out var imageName)
                    ? imageName
                    : resource.Name.ToLowerInvariant();
 
                // Only tag the image, don't push to a remote registry
                var containerRuntime = context.Services.GetRequiredService<IContainerRuntime>();
                await containerRuntime.TagImageAsync(localImageName, targetTag, context.CancellationToken).ConfigureAwait(false);
 
                await tagTask.CompleteAsync(
                    $"Successfully tagged **{resource.Name}** as `{targetTag}`",
                    CompletionState.Completed,
                    context.CancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                await tagTask.CompleteAsync(
                    $"Failed to tag **{resource.Name}**: {ex.Message}",
                    CompletionState.CompletedWithError,
                    context.CancellationToken).ConfigureAwait(false);
                throw;
            }
        }
    }
 
    private static async Task PushImageToRemoteRegistryAsync(IResource resource, IContainerRegistry registry, PipelineStepContext context)
    {
        var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false)
            ?? throw new InvalidOperationException("Failed to retrieve container registry information.");
 
        IValueProvider cir = new ContainerImageReference(resource);
        var targetTag = await cir.GetValueAsync(new ValueProviderContext { ExecutionContext = context.ExecutionContext }, context.CancellationToken).ConfigureAwait(false);
 
        var pushTask = await context.ReportingStep.CreateTaskAsync(
            $"Pushing **{resource.Name}** to **{registryName}**",
            context.CancellationToken).ConfigureAwait(false);
 
        await using (pushTask.ConfigureAwait(false))
        {
            try
            {
                if (targetTag is null)
                {
                    throw new InvalidOperationException($"Failed to get target tag for {resource.Name}");
                }
 
                var containerImageManager = context.Services.GetRequiredService<IResourceContainerImageManager>();
                await containerImageManager.PushImageAsync(resource, context.CancellationToken).ConfigureAwait(false);
 
                await pushTask.CompleteAsync(
                    $"Successfully pushed **{resource.Name}** to `{targetTag}`",
                    CompletionState.Completed,
                    context.CancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                await pushTask.CompleteAsync(
                    $"Failed to push **{resource.Name}**: {ex.Message}",
                    CompletionState.CompletedWithError,
                    context.CancellationToken).ConfigureAwait(false);
                throw;
            }
        }
    }
}