File: Publishing\DockerContainerRuntime.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 ASPIREPIPELINES003
#pragma warning disable ASPIRECONTAINERRUNTIME001
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp.Process;
using Aspire.Shared;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Publishing;
 
internal sealed class DockerContainerRuntime : ContainerRuntimeBase<DockerContainerRuntime>
{
    public DockerContainerRuntime(ILogger<DockerContainerRuntime> logger, IProcessRunner processRunner) : base(logger, processRunner)
    {
    }
 
    protected override string RuntimeExecutable => KnownContainerRuntimes.Docker;
    public override string Name => "Docker";
    private async Task RunDockerBuildAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken)
    {
        var imageName = !string.IsNullOrEmpty(options?.Tag)
            ? $"{options.ImageName}:{options.Tag}"
            : options?.ImageName ?? throw new ArgumentException("ImageName must be provided in options.", nameof(options));
 
        string? builderName = null;
        var resourceName = ResourceExtensions.FlattenContainerImageName(imageName);
 
        // Docker requires a custom buildkit instance for the image when
        // targeting the OCI format so we construct it and remove it here.
        if (options?.ImageFormat == ContainerImageFormat.Oci)
        {
            if (string.IsNullOrEmpty(options?.OutputPath))
            {
                throw new ArgumentException("OutputPath must be provided when ImageFormat is Oci.", nameof(options));
            }
 
            builderName = $"{resourceName}-builder";
            await CreateBuildkitInstanceAsync(builderName, cancellationToken).ConfigureAwait(false);
        }
 
        try
        {
            var arguments = $"buildx build --file \"{dockerfilePath}\" --tag \"{imageName}\"";
 
            // Use the specific builder for OCI builds
            if (!string.IsNullOrEmpty(builderName))
            {
                arguments += $" --builder \"{builderName}\"";
            }
 
            // Add platform support if specified
            if (options?.TargetPlatform is not null)
            {
                arguments += $" --platform \"{options.TargetPlatform.Value.ToRuntimePlatformString()}\"";
            }
 
            // Add output format support if specified
            if (options?.ImageFormat is not null || !string.IsNullOrEmpty(options?.OutputPath))
            {
                var outputType = options?.ImageFormat switch
                {
                    ContainerImageFormat.Oci => "type=oci",
                    ContainerImageFormat.Docker => "type=docker",
                    null => "type=docker",
                    _ => throw new ArgumentOutOfRangeException(nameof(options), options.ImageFormat, "Invalid container image format")
                };
 
                if (!string.IsNullOrEmpty(options?.OutputPath))
                {
                    var archivePath = ResourceExtensions.GetContainerImageArchivePath(options.OutputPath, imageName);
                    outputType += $",dest={archivePath}";
                }
 
                arguments += $" --output \"{outputType}\"";
            }
 
            // Add build arguments if specified
            arguments += BuildArgumentsString(buildArguments);
 
            // Add build secrets if specified
            arguments += BuildSecretsString(buildSecrets);
 
            // Add stage if specified
            arguments += BuildStageString(stage);
 
            arguments += $" \"{contextPath}\"";
 
            // Prepare environment variables for build secrets
            var environmentVariables = new Dictionary<string, string>();
            foreach (var buildSecret in buildSecrets)
            {
                if (buildSecret.Value.Type == BuildImageSecretType.Environment && buildSecret.Value.Value is not null)
                {
                    environmentVariables[buildSecret.Key.ToUpperInvariant()] = buildSecret.Value.Value;
                }
            }
 
            var processResult = await ExecuteContainerCommandWithResultAsync(
                arguments,
                "Docker build for {ImageName} failed with exit code {ExitCode}.",
                "Docker build for {ImageName} succeeded.",
                cancellationToken,
                new object[] { imageName },
                environmentVariables,
                retainOutput: true).ConfigureAwait(false);
 
            if (processResult.ExitCode != 0)
            {
                throw new ProcessFailedException(
                    $"Docker build failed with exit code {processResult.ExitCode}.",
                    processResult.ExitCode,
                    processResult.ProcessOutput,
                    processResult.TotalProcessOutputLineCount);
            }
        }
        finally
        {
            // Clean up the buildkit instance if we created one
            if (!string.IsNullOrEmpty(builderName))
            {
                await RemoveBuildkitInstanceAsync(builderName, cancellationToken).ConfigureAwait(false);
            }
        }
    }
 
    public override async Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken)
    {
        // Verify buildx is available before attempting a Dockerfile build
        if (!await CheckDockerBuildxAsync(cancellationToken).ConfigureAwait(false))
        {
            throw new DistributedApplicationException(
                "Docker buildx is not available. Install the buildx plugin and try again.");
        }
 
        // Normalize the context path to handle trailing slashes and relative paths
        var normalizedContextPath = Path.GetFullPath(contextPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
 
        await RunDockerBuildAsync(
            normalizedContextPath,
            dockerfilePath,
            options,
            buildArguments,
            buildSecrets,
            stage,
            cancellationToken).ConfigureAwait(false);
    }
 
    public override async Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
    {
        return await CheckDockerDaemonAsync(cancellationToken).ConfigureAwait(false);
    }
 
    private async Task<bool> CheckDockerDaemonAsync(CancellationToken cancellationToken)
    {
        try
        {
            var exitCode = await ExecuteContainerCommandWithExitCodeAsync(
                "container ls -n 1",
                "Docker daemon is not running. Exit code: {ExitCode}.",
                "Docker daemon is running.",
                cancellationToken,
                Array.Empty<object>()).ConfigureAwait(false);
 
            return exitCode == 0;
        }
        catch
        {
            return false;
        }
    }
 
    private async Task<bool> CheckDockerBuildxAsync(CancellationToken cancellationToken)
    {
        try
        {
            var exitCode = await ExecuteContainerCommandWithExitCodeAsync(
                "buildx version",
                "Docker buildx version failed with exit code {ExitCode}.",
                "Docker buildx is available.",
                cancellationToken,
                Array.Empty<object>()).ConfigureAwait(false);
 
            return exitCode == 0;
        }
        catch
        {
            return false;
        }
    }
 
    private async Task CreateBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
    {
        var arguments = $"buildx create --name \"{builderName}\" --driver docker-container";
        var processResult = await ExecuteContainerCommandWithResultAsync(
            arguments,
            "Failed to create buildkit instance {BuilderName} with exit code {ExitCode}.",
            "Successfully created buildkit instance {BuilderName}.",
            cancellationToken,
            new object[] { builderName },
            retainOutput: true).ConfigureAwait(false);
 
        if (processResult.ExitCode != 0)
        {
            throw new ProcessFailedException(
                $"Failed to create buildkit instance '{builderName}' with exit code {processResult.ExitCode}.",
                processResult.ExitCode,
                processResult.ProcessOutput,
                processResult.TotalProcessOutputLineCount);
        }
    }
 
    private async Task<int> RemoveBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
    {
        var arguments = $"buildx rm \"{builderName}\"";
 
        return await ExecuteContainerCommandWithExitCodeAsync(
            arguments,
            "Failed to remove buildkit instance {BuilderName} with exit code {ExitCode}.",
            "Successfully removed buildkit instance {BuilderName}.",
            cancellationToken,
            new object[] { builderName }).ConfigureAwait(false);
    }
}