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 ASPIREPUBLISHERS001
 
using Aspire.Hosting.Dcp.Process;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Publishing;
 
internal sealed class DockerContainerRuntime(ILogger<DockerContainerRuntime> logger) : IContainerRuntime
{
    public string Name => "Docker";
    private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken)
    {
        string? builderName = null;
        var resourceName = imageName.Replace('/', '-').Replace(':', '-');
 
        // 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";
            var createBuilderResult = await CreateBuildkitInstanceAsync(builderName, cancellationToken).ConfigureAwait(false);
 
            if (createBuilderResult != 0)
            {
                logger.LogError("Failed to create buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, createBuilderResult);
                return createBuilderResult;
            }
        }
 
        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))
                {
                    outputType += $",dest={Path.Combine(options.OutputPath, resourceName)}.tar";
                }
 
                arguments += $" --output \"{outputType}\"";
            }
 
            arguments += $" \"{contextPath}\"";
 
            var spec = new ProcessSpec("docker")
            {
                Arguments = arguments,
                OnOutputData = output =>
                {
                    logger.LogInformation("docker buildx (stdout): {Output}", output);
                },
                OnErrorData = error =>
                {
                    logger.LogInformation("docker buildx (stderr): {Error}", error);
                },
                ThrowOnNonZeroReturnCode = false,
                InheritEnv = true
            };
 
            logger.LogInformation("Running Docker CLI with arguments: {ArgumentList}", 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("docker buildx for {ImageName} failed with exit code {ExitCode}.", imageName, processResult.ExitCode);
                    return processResult.ExitCode;
                }
 
                logger.LogInformation("docker buildx for {ImageName} succeeded.", imageName);
                return processResult.ExitCode;
            }
        }
        finally
        {
            // Clean up the buildkit instance if we created one
            if (!string.IsNullOrEmpty(builderName))
            {
                await RemoveBuildkitInstanceAsync(builderName, cancellationToken).ConfigureAwait(false);
            }
        }
    }
 
    public async Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken)
    {
        var exitCode = await RunDockerBuildAsync(
            contextPath,
            dockerfilePath,
            imageName,
            options,
            cancellationToken).ConfigureAwait(false);
 
        if (exitCode != 0)
        {
            throw new DistributedApplicationException($"Docker build failed with exit code {exitCode}.");
        }
    }
 
    public async Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken)
    {
        // First check if Docker daemon is running using the same check that DCP uses
        if (!await CheckDockerDaemonAsync(cancellationToken).ConfigureAwait(false))
        {
            return false;
        }
 
        // Then check if Docker buildx is available
        return await CheckDockerBuildxAsync(cancellationToken).ConfigureAwait(false);
    }
 
    private async Task<bool> CheckDockerDaemonAsync(CancellationToken cancellationToken)
    {
        var dockerRunningSpec = new ProcessSpec("docker")
        {
            Arguments = "container ls -n 1",
            OnOutputData = output =>
            {
                logger.LogInformation("docker container ls (stdout): {Output}", output);
            },
            OnErrorData = error =>
            {
                logger.LogInformation("docker container ls (stderr): {Error}", error);
            },
            ThrowOnNonZeroReturnCode = false,
            InheritEnv = true
        };
 
        logger.LogInformation("Checking if Docker daemon is running with arguments: {ArgumentList}", dockerRunningSpec.Arguments);
        var (pendingDockerResult, dockerDisposable) = ProcessUtil.Run(dockerRunningSpec);
 
        await using (dockerDisposable)
        {
            var dockerResult = await pendingDockerResult.WaitAsync(cancellationToken).ConfigureAwait(false);
 
            if (dockerResult.ExitCode != 0)
            {
                logger.LogError("Docker daemon is not running. Exit code: {ExitCode}.", dockerResult.ExitCode);
                return false;
            }
 
            logger.LogInformation("Docker daemon is running.");
            return true;
        }
    }
 
    private async Task<bool> CheckDockerBuildxAsync(CancellationToken cancellationToken)
    {
        var buildxSpec = new ProcessSpec("docker")
        {
            Arguments = "buildx version",
            OnOutputData = output =>
            {
                logger.LogInformation("docker buildx version (stdout): {Output}", output);
            },
            OnErrorData = error =>
            {
                logger.LogInformation("docker buildx version (stderr): {Error}", error);
            },
            ThrowOnNonZeroReturnCode = false,
            InheritEnv = true
        };
 
        logger.LogInformation("Checking Docker buildx with arguments: {ArgumentList}", buildxSpec.Arguments);
        var (pendingBuildxResult, buildxDisposable) = ProcessUtil.Run(buildxSpec);
 
        await using (buildxDisposable)
        {
            var buildxResult = await pendingBuildxResult.WaitAsync(cancellationToken).ConfigureAwait(false);
 
            if (buildxResult.ExitCode != 0)
            {
                logger.LogError("Docker buildx version failed with exit code {ExitCode}.", buildxResult.ExitCode);
                return false;
            }
 
            logger.LogInformation("Docker buildx is available and running.");
            return true;
        }
    }
 
    private async Task<int> CreateBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
    {
        var arguments = $"buildx create --name \"{builderName}\" --driver docker-container";
 
        var spec = new ProcessSpec("docker")
        {
            Arguments = arguments,
            OnOutputData = output =>
            {
                logger.LogInformation("docker buildx create (stdout): {Output}", output);
            },
            OnErrorData = error =>
            {
                logger.LogInformation("docker buildx create (stderr): {Error}", error);
            },
            ThrowOnNonZeroReturnCode = false,
            InheritEnv = true
        };
 
        logger.LogInformation("Creating buildkit instance with arguments: {ArgumentList}", 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("Failed to create buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, processResult.ExitCode);
            }
            else
            {
                logger.LogInformation("Successfully created buildkit instance {BuilderName}.", builderName);
            }
 
            return processResult.ExitCode;
        }
    }
 
    private async Task<int> RemoveBuildkitInstanceAsync(string builderName, CancellationToken cancellationToken)
    {
        var arguments = $"buildx rm \"{builderName}\"";
 
        var spec = new ProcessSpec("docker")
        {
            Arguments = arguments,
            OnOutputData = output =>
            {
                logger.LogInformation("docker buildx rm (stdout): {Output}", output);
            },
            OnErrorData = error =>
            {
                logger.LogInformation("docker buildx rm (stderr): {Error}", error);
            },
            ThrowOnNonZeroReturnCode = false,
            InheritEnv = true
        };
 
        logger.LogInformation("Removing buildkit instance with arguments: {ArgumentList}", spec.Arguments);
        var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
 
        await using (processDisposable)
        {
            var processResult = await pendingProcessResult
                .WaitAsync(cancellationToken)
                .ConfigureAwait(false);
 
            if (processResult.ExitCode != 0)
            {
                logger.LogWarning("Failed to remove buildkit instance {BuilderName} with exit code {ExitCode}.", builderName, processResult.ExitCode);
            }
            else
            {
                logger.LogInformation("Successfully removed buildkit instance {BuilderName}.", builderName);
            }
 
            return processResult.ExitCode;
        }
    }
}