File: LocalDaemons\ContainerRuntimes.cs
Web Access
Project: src\sdk\src\Containers\Microsoft.NET.Build.Containers\Microsoft.NET.Build.Containers.csproj (Microsoft.NET.Build.Containers)
// 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 System.Text.Json;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.NET.Build.Containers.Resources;

namespace Microsoft.NET.Build.Containers;

internal abstract class ContainerRuntimeBase(ContainerRuntimeOperations operations, ILogger logger) : IContainerRuntime
{
    private bool _wasProbed;

    protected ContainerRuntimeOperations Operations { get; } = operations;

    protected ILogger Logger { get; } = logger;

    protected abstract string Command { get; }

    protected virtual string ProbeArguments => "version";

    /// <inheritdoc />
    public async Task<bool> ProbeAsync(CancellationToken cancellationToken)
    {
        _wasProbed = await Operations.ProbeCommandAsync(Command, ProbeArguments, cancellationToken).ConfigureAwait(false);
        return _wasProbed;
    }

    /// <inheritdoc />
    public virtual async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
        => _wasProbed || await ProbeAsync(cancellationToken).ConfigureAwait(false);

    /// <inheritdoc />
    public virtual string GetManifestMediaType(string defaultManifestMediaType, KnownImageFormats? imageFormat)
        => GetDefaultManifestMediaType(defaultManifestMediaType, imageFormat);

    /// <inheritdoc />
    public abstract ContainerRuntimeKind GetTelemetryValue();

    /// <inheritdoc />
    public virtual Task LoadAsync(
        BuiltImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken)
        => Operations.LoadFromStandardInputAsync(
            Command,
            ["load"],
            image,
            sourceReference,
            destinationReference,
            ContainerArchive.WriteDockerImageToStreamAsync,
            cancellationToken);

    /// <inheritdoc />
    public abstract Task LoadAsync(
        MultiArchImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken);

    internal static string GetDefaultManifestMediaType(string defaultManifestMediaType, KnownImageFormats? imageFormat)
        => imageFormat switch
        {
            null => defaultManifestMediaType,
            KnownImageFormats.Docker => SchemaTypes.DockerManifestV2,
            KnownImageFormats.OCI => SchemaTypes.OciManifestV1,
            _ => defaultManifestMediaType
        };
}

internal sealed class DockerContainerRuntime(ContainerRuntimeOperations operations, ILogger logger) : ContainerRuntimeBase(operations, logger)
{
    protected override string Command => ContainerRuntime.DockerCommand;

    /// <inheritdoc />
    public override Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
    {
        JsonDocument config = GetDockerConfig();
        if (!config.RootElement.TryGetProperty("ServerErrors", out JsonElement errorProperty) ||
            errorProperty.ValueKind == JsonValueKind.Array && errorProperty.GetArrayLength() == 0)
        {
            return Task.FromResult(true);
        }

        string messages = string.Join(Environment.NewLine, errorProperty.EnumerateArray());
        Logger.LogError($"The daemon server reported errors: {messages}");
        return Task.FromResult(false);
    }

    /// <inheritdoc />
    public override ContainerRuntimeKind GetTelemetryValue() => ContainerRuntimeKind.Docker;

    /// <inheritdoc />
    public override Task LoadAsync(
        MultiArchImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken)
    {
        if (!IsContainerdStoreEnabled())
        {
            throw new DockerLoadException(Strings.ImageLoadFailed_ContainerdStoreDisabled);
        }

        return Operations.LoadFromStandardInputAsync(
            Command,
            ["load"],
            image,
            sourceReference,
            destinationReference,
            ContainerArchive.WriteMultiArchOciImageToStreamAsync,
            cancellationToken);
    }

    internal static bool IsPodmanAlias()
    {
        try
        {
            JsonElement dockerInfo = GetDockerConfig().RootElement;
            bool hasDockerProperty =
                dockerInfo.TryGetProperty("DockerRootDir", out JsonElement dockerRootDir) &&
                dockerRootDir.GetString() is not null;
            bool hasPodmanProperty =
                dockerInfo.TryGetProperty("host", out JsonElement host) &&
                host.TryGetProperty("buildahVersion", out JsonElement buildahVersion) &&
                buildahVersion.GetString() is not null;
            return !hasDockerProperty && hasPodmanProperty;
        }
        catch
        {
            return false;
        }
    }

    internal static JsonDocument GetDockerConfig()
    {
        string dockerPath = ContainerRuntimeOperations.FindFullPathFromPath(ContainerRuntime.DockerCommand);
        Process process = new()
        {
            StartInfo = new ProcessStartInfo(dockerPath, "info --format=\"{{json .}}\"")
        };

        try
        {
            Command dockerCommand = new(process);
            dockerCommand.CaptureStdOut();
            dockerCommand.CaptureStdErr();
            CommandResult result = dockerCommand.Execute();
            if (result.ExitCode != 0)
            {
                throw new DockerLoadException(Resource.FormatString(
                    nameof(Strings.DockerInfoFailed),
                    result.ExitCode,
                    result.StdOut,
                    result.StdErr));
            }

            return JsonDocument.Parse(result.StdOut ?? string.Empty);
        }
        catch (Exception e) when (e is not DockerLoadException)
        {
            throw new DockerLoadException(Resource.FormatString(nameof(Strings.DockerInfoFailed_Ex), e.Message));
        }
    }

    internal static bool IsInsecureRegistry(string registryDomain)
    {
        try
        {
            JsonElement rootElement = GetDockerConfig().RootElement;
            if (rootElement.TryGetProperty("RegistryConfig", out JsonElement registryConfig) &&
                registryConfig.ValueKind == JsonValueKind.Object &&
                registryConfig.TryGetProperty("IndexConfigs", out JsonElement indexConfigs) &&
                indexConfigs.ValueKind == JsonValueKind.Object)
            {
                foreach (JsonProperty property in indexConfigs.EnumerateObject())
                {
                    if (property.Value.ValueKind == JsonValueKind.Object &&
                        property.Value.TryGetProperty("Secure", out JsonElement secure) &&
                        !secure.GetBoolean() &&
                        property.Name.Equals(registryDomain, StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
            }

            if (rootElement.TryGetProperty("registries", out JsonElement registries) &&
                registries.ValueKind == JsonValueKind.Object)
            {
                foreach (JsonProperty property in registries.EnumerateObject())
                {
                    if (property.Value.ValueKind == JsonValueKind.Object &&
                        property.Value.TryGetProperty("Insecure", out JsonElement insecure) &&
                        insecure.GetBoolean() &&
                        property.Name.Equals(registryDomain, StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        catch (DockerLoadException)
        {
            return false;
        }
    }

    internal static bool IsContainerdStoreEnabled()
    {
        try
        {
            if (!GetDockerConfig().RootElement.TryGetProperty("DriverStatus", out JsonElement driverStatus) ||
                driverStatus.ValueKind != JsonValueKind.Array)
            {
                return false;
            }

            foreach (JsonElement item in driverStatus.EnumerateArray())
            {
                if (item.ValueKind != JsonValueKind.Array || item.GetArrayLength() != 2)
                {
                    continue;
                }

                JsonElement[] values = item.EnumerateArray().ToArray();
                if (values[0].GetString() == "driver-type" &&
                    values[1].GetString()!.StartsWith("io.containerd.snapshotter"))
                {
                    return true;
                }
            }
            return false;
        }
        catch
        {
            return false;
        }
    }
}

internal sealed class PodmanContainerRuntime(ContainerRuntimeOperations operations, ILogger logger) : ContainerRuntimeBase(operations, logger)
{
    protected override string Command => ContainerRuntime.PodmanCommand;

    /// <inheritdoc />
    public override ContainerRuntimeKind GetTelemetryValue() => ContainerRuntimeKind.Podman;

    /// <inheritdoc />
    public override Task LoadAsync(
        MultiArchImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        string commandPath = Operations.FindFullCommandPath(Command);
        var createdImages = new List<string>();
        return LoadAsyncCore();

        async Task LoadAsyncCore()
        {
            try
            {
                string firstTag = destinationReference.Tags.First();
                string manifestName = $"{destinationReference.Repository}:{firstTag}";
                await RunAndIgnoreAsync($"rmi {manifestName}");
                await RunAsync($"manifest create {manifestName}");
                createdImages.Add(manifestName);

                Debug.Assert(image.Images is not null);
                foreach (BuiltImage architectureImage in image.Images)
                {
                    string repo = $"{destinationReference.Repository}-{architectureImage.Architecture}";
                    string tag = $"{repo}:{firstTag}";
                    var architectureDestination = new DestinationImageReference(destinationReference.LocalRegistry!, repo, [firstTag]);

                    await Operations.LoadFromStandardInputAsync(
                        Command,
                        ["load"],
                        architectureImage,
                        sourceReference,
                        architectureDestination,
                        ContainerArchive.WriteDockerImageToStreamAsync,
                        cancellationToken).ConfigureAwait(false);
                    createdImages.Add(tag);
                    await RunAsync($"manifest add {manifestName} {tag}");
                }

                foreach (string tag in destinationReference.Tags.Skip(1))
                {
                    string additionalTag = $"{destinationReference.Repository}:{tag}";
                    await RunAsync($"tag {manifestName} {additionalTag}");
                    createdImages.Add(additionalTag);
                }
            }
            catch
            {
                foreach (string createdImage in createdImages)
                {
                    await RunAndIgnoreAsync($"rmi {createdImage}");
                }
                throw;
            }
        }

        async Task RunAsync(string arguments)
        {
            (int exitCode, string stderr) = await ContainerRuntimeOperations.RunProcessAsync(commandPath, arguments, cancellationToken);
            if (exitCode != 0)
            {
                throw new DockerLoadException(Resource.FormatString(nameof(Strings.ImageLoadFailed), stderr));
            }
        }

        async Task RunAndIgnoreAsync(string arguments)
        {
            try
            {
                _ = await ContainerRuntimeOperations.RunProcessAsync(commandPath, arguments, CancellationToken.None);
            }
            catch
            { }
        }
    }
}

internal sealed class WslcContainerRuntime(ContainerRuntimeOperations operations, ILogger logger) : ContainerRuntimeBase(operations, logger)
{
    protected override string Command => ContainerRuntime.WslcCommand;

    // Listing images verifies that WSLC can resolve and start its container session.
    protected override string ProbeArguments => "image ls";

    /// <inheritdoc />
    public override ContainerRuntimeKind GetTelemetryValue() => ContainerRuntimeKind.Wslc;

    /// <inheritdoc />
    public override Task LoadAsync(
        BuiltImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken)
        => Operations.LoadFromFileAsync(
            Command,
            archivePath => ["image", "load", "--input", archivePath],
            image,
            sourceReference,
            destinationReference,
            ContainerArchive.WriteDockerImageToStreamAsync,
            cancellationToken);

    /// <inheritdoc />
    public override Task LoadAsync(
        MultiArchImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken)
    {
        // Track WSLC support for importing multi-architecture images at https://github.com/microsoft/WSL/issues/41123.
        throw new DockerLoadException(Strings.ImageLoadFailed_WslcMultiArchUnsupported);
    }
}

internal sealed class MacOSContainerRuntime(ContainerRuntimeOperations operations, ILogger logger) : ContainerRuntimeBase(operations, logger)
{
    protected override string Command => ContainerRuntime.MacOSContainerCommand;

    // The executable can be installed while the macOS container services are stopped.
    protected override string ProbeArguments => "system status";

    /// <inheritdoc />
    public override ContainerRuntimeKind GetTelemetryValue() => ContainerRuntimeKind.MacOSContainer;

    /// <inheritdoc />
    public override Task LoadAsync(
        BuiltImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken)
        => Operations.LoadFromFileAsync(
            Command,
            archivePath => ["image", "load", "--input", archivePath],
            image,
            sourceReference,
            destinationReference,
            ContainerArchive.WriteOciImageToStreamAsync,
            cancellationToken);

    /// <inheritdoc />
    public override Task LoadAsync(
        MultiArchImage image,
        SourceImageReference sourceReference,
        DestinationImageReference destinationReference,
        CancellationToken cancellationToken)
        => Operations.LoadFromFileAsync(
            Command,
            archivePath => ["image", "load", "--input", archivePath],
            image,
            sourceReference,
            destinationReference,
            ContainerArchive.WriteMultiArchOciImageToStreamAsync,
            cancellationToken);
}