|
// 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 ASPIREPUBLISHERS001
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Dcp.Process;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Aspire.Hosting.Publishing;
/// <summary>
/// Provides a service to publishers for building containers that represent a resource.
/// </summary>
[Experimental("ASPIREPUBLISHERS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public interface IResourceContainerImageBuilder
{
/// <summary>
/// Builds a container that represents the specified resource.
/// </summary>
/// <param name="resource">The resource to build.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task BuildImageAsync(IResource resource, CancellationToken cancellationToken);
/// <summary>
/// Builds container images for a collection of resources.
/// </summary>
/// <param name="resources">The resources to build images for.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns></returns>
Task BuildImagesAsync(IEnumerable<IResource> resources, CancellationToken cancellationToken);
}
internal sealed class ResourceContainerImageBuilder(
ILogger<ResourceContainerImageBuilder> logger,
IOptions<DcpOptions> dcpOptions,
IServiceProvider serviceProvider,
IPublishingActivityProgressReporter activityReporter) : IResourceContainerImageBuilder
{
public async Task BuildImagesAsync(IEnumerable<IResource> resources, CancellationToken cancellationToken)
{
var step = await activityReporter.CreateStepAsync(
"Building container images for resources",
cancellationToken).ConfigureAwait(false);
foreach (var resource in resources)
{
// TODO: Consider parallelizing this.
await BuildImageAsync(step, resource, cancellationToken).ConfigureAwait(false);
}
await activityReporter.CompleteStepAsync(step, "Building container images completed", cancellationToken: cancellationToken).ConfigureAwait(false);
}
public Task BuildImageAsync(IResource resource, CancellationToken cancellationToken)
{
return BuildImageAsync(step: null, resource, cancellationToken);
}
private async Task BuildImageAsync(PublishingStep? step, IResource resource, CancellationToken cancellationToken)
{
logger.LogInformation("Building container image for resource {Resource}", resource.Name);
if (resource is ProjectResource)
{
// If it is a project resource we need to build the container image
// using the .NET SDK.
await BuildProjectContainerImageAsync(
resource,
step,
cancellationToken).ConfigureAwait(false);
return;
}
else if (resource.TryGetLastAnnotation<ContainerImageAnnotation>(out var containerImageAnnotation))
{
if (resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out var dockerfileBuildAnnotation))
{
// This is a container resource so we'll use the container runtime to build the image
await BuildContainerImageFromDockerfileAsync(
resource.Name,
dockerfileBuildAnnotation.ContextPath,
dockerfileBuildAnnotation.DockerfilePath,
containerImageAnnotation.Image,
step,
cancellationToken).ConfigureAwait(false);
return;
}
}
else
{
throw new NotSupportedException($"The resource type '{resource.GetType().Name}' is not supported.");
}
}
private async Task BuildProjectContainerImageAsync(IResource resource, PublishingStep? step, CancellationToken cancellationToken)
{
var publishingTask = await CreateTaskAsync(
step,
$"Building image: {resource.Name}",
cancellationToken
).ConfigureAwait(false);
// This is a resource project so we'll use the .NET SDK to build the container image.
if (!resource.TryGetLastAnnotation<IProjectMetadata>(out var projectMetadata))
{
throw new DistributedApplicationException($"The resource '{projectMetadata}' does not have a project metadata annotation.");
}
var spec = new ProcessSpec("dotnet")
{
Arguments = $"publish {projectMetadata.ProjectPath} --configuration Release /t:PublishContainer /p:ContainerRepository={resource.Name}",
OnOutputData = output =>
{
logger.LogInformation("dotnet publish {ProjectPath} (stdout): {Output}", projectMetadata.ProjectPath, output);
},
OnErrorData = error =>
{
logger.LogError("dotnet publish {ProjectPath} (stderr): {Error}", projectMetadata.ProjectPath, error);
}
};
logger.LogInformation(
"Starting .NET CLI with arguments: {Arguments}",
string.Join(" ", spec.Arguments)
);
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
await using (processDisposable)
{
var processResult = await pendingProcessResult
.WaitAsync(cancellationToken)
.ConfigureAwait(false);
if (processResult.ExitCode != 0)
{
logger.LogError("dotnet publish for project {ProjectPath} failed with exit code {ExitCode}.", projectMetadata.ProjectPath, processResult.ExitCode);
await CompleteTaskAsync(
publishingTask,
TaskCompletionState.CompletedWithError,
$"Building image: {resource.Name} failed",
cancellationToken).ConfigureAwait(false);
throw new DistributedApplicationException($"Failed to build container image.");
}
else
{
await CompleteTaskAsync(
publishingTask,
TaskCompletionState.Completed,
$"Building image: {resource.Name} completed",
cancellationToken).ConfigureAwait(false);
logger.LogDebug(
".NET CLI completed with exit code: {ExitCode}",
processResult.ExitCode);
}
}
}
private async Task BuildContainerImageFromDockerfileAsync(string resourceName, string contextPath, string dockerfilePath, string imageName, PublishingStep? step, CancellationToken cancellationToken)
{
var publishingTask = await CreateTaskAsync(
step,
$"Building image: {resourceName}",
cancellationToken
).ConfigureAwait(false);
try
{
var containerRuntime = dcpOptions.Value.ContainerRuntime switch
{
string rt => serviceProvider.GetRequiredKeyedService<IContainerRuntime>(rt),
null => serviceProvider.GetRequiredKeyedService<IContainerRuntime>("docker")
};
await containerRuntime.BuildImageAsync(
contextPath,
dockerfilePath,
imageName,
cancellationToken).ConfigureAwait(false);
await CompleteTaskAsync(
publishingTask,
TaskCompletionState.Completed,
$"Building image: {resourceName} completed",
cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to build container image from Dockerfile.");
await CompleteTaskAsync(
publishingTask,
TaskCompletionState.CompletedWithError,
$"Building image: {resourceName} failed",
cancellationToken).ConfigureAwait(false);
throw;
}
}
private async Task<PublishingTask?> CreateTaskAsync(
PublishingStep? step,
string description,
CancellationToken cancellationToken)
{
if (step is null)
{
return null;
}
return await activityReporter.CreateTaskAsync(step, description, cancellationToken).ConfigureAwait(false);
}
private async Task CompleteTaskAsync(
PublishingTask? task,
TaskCompletionState state,
string description,
CancellationToken cancellationToken)
{
if (task is null)
{
return;
}
await activityReporter.CompleteTaskAsync(task, state, description, cancellationToken).ConfigureAwait(false);
}
}
|