File: Tasks\CreateImageIndex.cs
Web Access
Project: ..\..\..\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.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Build.Framework;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.MSBuild;
using Microsoft.NET.Build.Containers.Resources;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Task = System.Threading.Tasks.Task;
 
namespace Microsoft.NET.Build.Containers.Tasks;
 
public sealed partial class CreateImageIndex : Microsoft.Build.Utilities.Task, ICancelableTask, IDisposable
{
    private readonly CancellationTokenSource _cancellationTokenSource = new();
 
    public void Cancel() => _cancellationTokenSource.Cancel();
 
    public void Dispose()
    {
        _cancellationTokenSource.Dispose();
    }
 
    private bool IsLocalPull => string.IsNullOrWhiteSpace(BaseRegistry);
 
    public override bool Execute()
    {
        try
        {
            Task.Run(() => ExecuteAsync(_cancellationTokenSource.Token)).GetAwaiter().GetResult();
        }
        catch (TaskCanceledException ex)
        {
            Log.LogWarningFromException(ex);
        }
        catch (OperationCanceledException ex)
        {
            Log.LogWarningFromException(ex);
        }
        return !Log.HasLoggedErrors;
    }
 
    internal async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        if (LocalRegistry == "Podman")
        {
            Log.LogError(Strings.ImageIndex_PodmanNotSupported);
            return false;
        }
 
        using MSBuildLoggerProvider loggerProvider = new(Log);
        ILoggerFactory msbuildLoggerFactory = new LoggerFactory(new[] { loggerProvider });
        ILogger logger = msbuildLoggerFactory.CreateLogger<CreateImageIndex>();
 
        RegistryMode sourceRegistryMode = BaseRegistry.Equals(OutputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull;
        Registry? sourceRegistry = IsLocalPull ? null : new Registry(BaseRegistry, logger, sourceRegistryMode);
        SourceImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag, BaseImageDigest);
 
        DestinationImageReference destinationImageReference = DestinationImageReference.CreateFromSettings(
            Repository,
            ImageTags,
            msbuildLoggerFactory,
            ArchiveOutputPath,
            OutputRegistry,
            LocalRegistry);
 
        var images = ParseImages(destinationImageReference.Kind);
        if (Log.HasLoggedErrors)
        {
            return false;
        }
 
        var multiArchImage = CreateMultiArchImage(images, destinationImageReference.Kind);
 
        GeneratedImageIndex = multiArchImage.ImageIndex;
        GeneratedArchiveOutputPath = ArchiveOutputPath;
 
        logger.LogInformation(Strings.BuildingImageIndex, destinationImageReference, string.Join(", ", images.Select(i => i.ManifestDigest)));
 
        var telemetry = new Telemetry(sourceImageReference, destinationImageReference, Log);
 
        await ImagePublisher.PublishImageAsync(multiArchImage, sourceImageReference, destinationImageReference, Log, telemetry, cancellationToken)
            .ConfigureAwait(false); 
 
        return !Log.HasLoggedErrors;
    }
 
    private BuiltImage[] ParseImages(DestinationImageReferenceKind destinationKind)
    {
        var images = new BuiltImage[GeneratedContainers.Length];
 
        for (int i = 0; i < GeneratedContainers.Length; i++)
        {
            var unparsedImage = GeneratedContainers[i];
 
            string config = unparsedImage.GetMetadata("Configuration");
            string manifestDigest = unparsedImage.GetMetadata("ManifestDigest");
            string manifest = unparsedImage.GetMetadata("Manifest");
            string manifestMediaType = unparsedImage.GetMetadata("ManifestMediaType");
 
            if (string.IsNullOrEmpty(config) || string.IsNullOrEmpty(manifestDigest) || string.IsNullOrEmpty(manifest) || string.IsNullOrEmpty(manifestMediaType))
            {
                Log.LogError(Strings.InvalidImageMetadata);
                break;
            }
 
            (string architecture, string os) = GetArchitectureAndOsFromConfig(config);
 
            // We don't need ImageDigest, ImageSha, Layers for remote registry, as the individual images should be pushed already
            string? imageDigest = null;
            string? imageSha = null;
            List<ManifestLayer>? layers = null;
 
            if (destinationKind == DestinationImageReferenceKind.LocalRegistry)
            {
                var manifestV2 = JsonSerializer.Deserialize<ManifestV2>(manifest);
                if (manifestV2 == null)
                {
                    Log.LogError(Strings.InvalidImageManifest);
                    break;
                }
 
                imageDigest = manifestV2.Config.digest;
                imageSha = DigestUtils.GetShaFromDigest(imageDigest);
                layers = manifestV2.Layers;
            }     
 
            images[i] = new BuiltImage()
            {
                Config = config,
                ImageDigest = imageDigest,
                ImageSha = imageSha,
                Manifest = manifest,
                ManifestDigest = manifestDigest,
                ManifestMediaType = manifestMediaType,
                Layers = layers,
                OS = os,
                Architecture = architecture
            };
        }
 
        return images;
    }
 
    private (string, string) GetArchitectureAndOsFromConfig(string config)
    {
        var configJson = JsonNode.Parse(config) as JsonObject;
        if (configJson is null)
        {
            Log.LogError(Strings.InvalidImageConfig);
            return (string.Empty, string.Empty);
        }
        var architecture = configJson["architecture"]?.ToString();
        if (architecture is null)
        {
            Log.LogError(Strings.ImageConfigMissingArchitecture);
            return (string.Empty, string.Empty);
        } 
        var os = configJson["os"]?.ToString();
        if (os is null)
        {
            Log.LogError(Strings.ImageConfigMissingOs);
            return (string.Empty, string.Empty);
        }
        return (architecture, os);
    }
 
    private static MultiArchImage CreateMultiArchImage(BuiltImage[] images, DestinationImageReferenceKind destinationImageKind)
    {
        switch (destinationImageKind)
        {
            case DestinationImageReferenceKind.LocalRegistry:
                return new MultiArchImage()
                {
                    // For multi-arch we publish only oci-formatted image tarballs.
                    ImageIndex = ImageIndexGenerator.GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1),
                    ImageIndexMediaType = SchemaTypes.OciImageIndexV1,
                    Images = images
                };
            case DestinationImageReferenceKind.RemoteRegistry:
                (string imageIndex, string mediaType) = ImageIndexGenerator.GenerateImageIndex(images);
                return new MultiArchImage()
                {
                    ImageIndex = imageIndex,
                    ImageIndexMediaType = mediaType,
                    // For remote registry we don't need individual images, as they should be pushed already
                };
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}