File: Utils\EnvironmentChecker\DockerEngineCheck.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Utils.EnvironmentChecker;
 
/// <summary>
/// Checks if Docker Engine (vs Docker Desktop) is installed and provides tunnel guidance.
/// </summary>
internal sealed class DockerEngineCheck(ILogger<DockerEngineCheck> logger) : IEnvironmentCheck
{
    private static readonly TimeSpan s_processTimeout = TimeSpan.FromSeconds(10);
 
    public int Order => 50; // Process check - more expensive, runs after container runtime check
 
    public async Task<IReadOnlyList<EnvironmentCheckResult>> CheckAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            // Check if Docker is available
            var processInfo = new ProcessStartInfo
            {
                FileName = "docker",
                Arguments = "--version",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };
 
            using var process = Process.Start(processInfo);
            if (process is null)
            {
                // Docker not available, skip this check (ContainerRuntimeCheck handles this case)
                return [];
            }
 
            using var versionTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            versionTimeoutCts.CancelAfter(s_processTimeout);
 
            try
            {
                await process.WaitForExitAsync(versionTimeoutCts.Token);
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                process.Kill();
                return []; // Timeout, skip this check
            }
 
            if (process.ExitCode != 0)
            {
                // Docker not available, skip this check (ContainerRuntimeCheck handles this case)
                return [];
            }
 
            var isDockerDesktop = await IsDockerDesktopAsync(cancellationToken);
 
            if (isDockerDesktop)
            {
                return [new EnvironmentCheckResult
                {
                    Category = "container",
                    Name = "docker-engine",
                    Status = EnvironmentCheckStatus.Pass,
                    Message = "Docker Desktop detected"
                }];
            }
 
            // Docker Engine detected - check if tunnel is already enabled
            var tunnelEnabled = Environment.GetEnvironmentVariable("ASPIRE_ENABLE_CONTAINER_TUNNEL");
            if (string.Equals(tunnelEnabled, "true", StringComparison.OrdinalIgnoreCase))
            {
                return [new EnvironmentCheckResult
                {
                    Category = "container",
                    Name = "docker-engine",
                    Status = EnvironmentCheckStatus.Pass,
                    Message = "Docker Engine with container tunnel enabled"
                }];
            }
 
            // Warn about tunnel requirement
            return [new EnvironmentCheckResult
            {
                Category = "container",
                Name = "docker-engine",
                Status = EnvironmentCheckStatus.Warning,
                Message = "Docker Engine requires Aspire's container tunnel to allow containers to reach applications running on the host",
                Fix = "Set environment variable: ASPIRE_ENABLE_CONTAINER_TUNNEL=true",
                Link = "https://aka.ms/aspire-prerequisites#docker-engine"
            }];
        }
        catch (Exception ex)
        {
            logger.LogDebug(ex, "Error checking Docker Engine");
            // Skip this check on error (ContainerRuntimeCheck handles container availability)
            return [];
        }
    }
 
    private static async Task<bool> IsDockerDesktopAsync(CancellationToken cancellationToken)
    {
        try
        {
            // Check Docker context to see if it's Desktop
            var processInfo = new ProcessStartInfo
            {
                FileName = "docker",
                Arguments = "context inspect",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };
 
            using var process = Process.Start(processInfo);
            if (process is null)
            {
                return false;
            }
 
            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            timeoutCts.CancelAfter(s_processTimeout);
 
            try
            {
                var output = await process.StandardOutput.ReadToEndAsync(timeoutCts.Token);
                await process.WaitForExitAsync(timeoutCts.Token);
 
                // Docker Desktop context usually contains "desktop" in the name or endpoint
                return output.Contains("desktop", StringComparison.OrdinalIgnoreCase);
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                process.Kill();
                return false; // Timeout, assume not Docker Desktop
            }
        }
        catch
        {
            return false;
        }
    }
}