File: DockerRegistryManager.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Build.Containers.IntegrationTests\Microsoft.NET.Build.Containers.IntegrationTests.csproj (Microsoft.NET.Build.Containers.IntegrationTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
using Xunit.Sdk;
 
namespace Microsoft.NET.Build.Containers.IntegrationTests;
 
public class DockerRegistryManager
{
    public const string RuntimeBaseImage = "dotnet/runtime";
    public const string AspNetBaseImage = "dotnet/aspnet";
    public const string BaseImageSource = "mcr.microsoft.com";
    public const string Net6ImageTag = "6.0";
    public const string Net7ImageTag = "7.0";
    public const string Net8ImageTag = "8.0";
    public const string Net9ImageTag = "9.0";
    public const string Net9ImageDigest = "sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345";
    public const string Net8PreviewWindowsSpecificImageTag = $"{Net8ImageTag}-nanoserver-ltsc2022";
    public const string LocalRegistry = "localhost:5010";
    public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}/{RuntimeBaseImage}:{Net9ImageTag}";
    public const string FullyQualifiedBaseImageAspNet = $"{BaseImageSource}/{AspNetBaseImage}:{Net9ImageTag}";
    public const string FullyQualifiedBaseImageAspNetDigest = $"{BaseImageSource}/{AspNetBaseImage}@{Net9ImageDigest}";
    private static string? s_registryContainerId;
 
    private static string SDK_AzureContainerRegistryImage => "dotnetdhmirror-f8bzbjakh8cga6ab.azurecr.io/registry:2";
    private static string Docker_HubRegistryImage => "docker.io/library/registry:2";
 
    // TODO: some logic to pivot between this and Docker Hub
    private static string RegistryImageToUse => SDK_AzureContainerRegistryImage;
 
    internal class SameArchManifestPicker : IManifestPicker
    {
        public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary<string, PlatformSpecificManifest> manifestList, string runtimeIdentifier)
        {
            return manifestList.Values.SingleOrDefault(m => m.platform.os == "linux" && m.platform.architecture == "amd64");
        }
 
        public PlatformSpecificOciManifest? PickBestManifestForRid(IReadOnlyDictionary<string, PlatformSpecificOciManifest> manifestList, string runtimeIdentifier)
        {
            return manifestList.Values.SingleOrDefault(m => m.platform.os == "linux" && m.platform.architecture == "amd64");
        }
    }
 
    public static async Task StartAndPopulateDockerRegistry(ITestOutputHelper testOutput)
    {
        using TestLoggerFactory loggerFactory = new(testOutput);
 
        if (!new DockerCli(loggerFactory).IsAvailable())
        {
            throw new InvalidOperationException("Docker is not available, tests cannot run");
        }
 
        ILogger logger = loggerFactory.CreateLogger("Docker Registry Init");
 
        const int spawnRegistryMaxRetry = 5;
        int spawnRegistryDelay = 1000; //ms
        StringBuilder failureReasons = new();
 
        var pullRegistry = new Registry(BaseImageSource, logger, RegistryMode.Pull);
        var pushRegistry = new Registry(LocalRegistry, logger, RegistryMode.Push);
 
        for (int spawnRegistryAttempt = 1; spawnRegistryAttempt <= spawnRegistryMaxRetry; spawnRegistryAttempt++)
        {
            try
            {
                logger.LogInformation("Spawning local registry at '{registry}', attempt #{attempt}.", LocalRegistry, spawnRegistryAttempt);
 
                CommandResult processResult = ContainerCli.RunCommand(testOutput, "--rm", "--publish", "5010:5000", "--detach", RegistryImageToUse).Execute();
 
                processResult.Should().Pass().And.HaveStdOut();
 
                logger.LogInformation("StdOut: {stream}", processResult.StdOut);
                logger.LogInformation("StdErr: {stream}", processResult.StdErr);
 
                using var reader = new StringReader(processResult.StdOut!);
                s_registryContainerId = reader.ReadLine();
 
                EnsureRegistryLoaded(new Uri($"http://{LocalRegistry}"), s_registryContainerId, logger, testOutput);
 
                foreach (string? tag in new[] { Net6ImageTag, Net7ImageTag, Net8ImageTag, Net9ImageTag })
                {
                    logger.LogInformation("Pulling image '{repo}/{image}:{tag}'.", BaseImageSource, RuntimeBaseImage, tag);
                    string dotnetdll = System.Reflection.Assembly.GetExecutingAssembly().Location;
                    var ridjson = Path.Combine(Path.GetDirectoryName(dotnetdll)!, "RuntimeIdentifierGraph.json");
 
                    var image = await pullRegistry.GetImageManifestAsync(RuntimeBaseImage, tag, "linux-x64", new SameArchManifestPicker(), CancellationToken.None);
                    var source = new SourceImageReference(pullRegistry, RuntimeBaseImage, tag, null);
                    var dest = new DestinationImageReference(pushRegistry, RuntimeBaseImage, [tag]);
                    logger.LogInformation($"Pushing image for {BaseImageSource}/{RuntimeBaseImage}:{tag}");
                    await pushRegistry.PushAsync(image.Build(), source, dest, CancellationToken.None);
                    logger.LogInformation($"Pushed image  for {BaseImageSource}/{RuntimeBaseImage}:{tag}");
                }
 
                return;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Spawn registry attempt #{attempt} failed.", spawnRegistryAttempt);
                // logging is not easily available, so collect error messages to throw in final exception if needed.
                failureReasons.AppendLine($"Spawn registry attempt #{spawnRegistryAttempt} failed, {ex}");
 
                //stop registry, if started and ignore errors.
                if (!string.IsNullOrWhiteSpace(s_registryContainerId))
                {
                    try
                    {
                        ContainerCli.StopCommand(testOutput, s_registryContainerId).Execute();
                    }
                    catch (Exception ex2)
                    {
                        logger.LogError(ex2, "Failed to stop the registry {id}.", s_registryContainerId);
                    }
                }
 
                logger.LogInformation("Retrying after {delay} ms.", spawnRegistryDelay);
                Thread.Sleep(spawnRegistryDelay);
                spawnRegistryDelay *= 2;
            }
        }
        throw new InvalidOperationException($"The registry was not loaded after {spawnRegistryMaxRetry} retries. {failureReasons}");
    }
 
    public static void ShutdownDockerRegistry(ITestOutputHelper testOutput)
    {
        if (s_registryContainerId != null)
        {
            ContainerCli.StopCommand(testOutput, s_registryContainerId)
                .Execute()
                .Should().Pass();
        }
    }
 
    private static void EnsureRegistryLoaded(Uri registryBaseUri, string? containerRegistryId, ILogger logger, ITestOutputHelper testOutput)
    {
        const int registryLoadMaxRetry = 10;
        const int registryLoadTimeout = 1000; //ms
 
        using HttpClient client = new();
        using HttpRequestMessage request = new(HttpMethod.Get, new Uri(registryBaseUri, "/v2/"));
 
        logger.LogInformation("Checking if the registry '{registry}' is available.", registryBaseUri);
 
        int attempt = 1;
        while (attempt <= registryLoadMaxRetry)
        {
            //added an additional delay to allow registry to load
            Thread.Sleep(registryLoadTimeout);
 
            try
            {
                HttpResponseMessage response = client.Send(request);
 
                if (response.IsSuccessStatusCode)
                {
                    logger.LogInformation("The registry '{registry}' is available after {timeout} ms.", registryBaseUri, attempt * registryLoadTimeout);
                    return;
                }
                logger.LogWarning("The registry '{registry} is not loaded after {timeout} ms. Returned status code: {statusCode}.", registryBaseUri, attempt * registryLoadTimeout, response.StatusCode);
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "The registry '{registry} is not loaded after {timeout} ms.", registryBaseUri, attempt * registryLoadTimeout);
            }
            attempt++;
        }
        logger.LogError("The registry was not loaded after {timeout} ms.", registryLoadMaxRetry * registryLoadTimeout);
        if (string.IsNullOrWhiteSpace(containerRegistryId))
        {
            return;
        }
 
        //try to collect the logs from started registry for more info
        try
        {
            CommandResult logsResult = ContainerCli.LogsCommand(testOutput, containerRegistryId).Execute();
            logger.LogInformation("Registry logs: {stdout} {stderr}", logsResult.StdOut, logsResult.StdErr);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to gather logs from container {id}.", containerRegistryId);
        }
    }
}