File: ImageIndexGenerator.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.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.NET.Build.Containers.Resources;
 
namespace Microsoft.NET.Build.Containers;
 
internal static class ImageIndexGenerator
{
    /// <summary>
    /// Generates an image index from the given images.
    /// </summary>
    /// <param name="images">Images to generate image index from.</param>
    /// <returns>Returns json string of image index and image index mediaType.</returns>
    /// <exception cref="ArgumentException"></exception>
    /// <exception cref="NotSupportedException"></exception>
    internal static (string, string) GenerateImageIndex(BuiltImage[] images)
    {
        if (images.Length == 0)
        {
            throw new ArgumentException(Strings.ImagesEmpty);
        }
 
        string manifestMediaType = images[0].ManifestMediaType;
 
        if (!images.All(image => string.Equals(image.ManifestMediaType, manifestMediaType, StringComparison.OrdinalIgnoreCase)))
        {
            throw new ArgumentException(Strings.MixedMediaTypes);
        }
 
        if (manifestMediaType == SchemaTypes.DockerManifestV2)
        {
            return (GenerateImageIndex(images, SchemaTypes.DockerManifestV2, SchemaTypes.DockerManifestListV2), SchemaTypes.DockerManifestListV2);
        }
        else if (manifestMediaType == SchemaTypes.OciManifestV1)
        {
            return (GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1), SchemaTypes.OciImageIndexV1);
        }
        else
        {
            throw new NotSupportedException(string.Format(Strings.UnsupportedMediaType, manifestMediaType));
        }
    }
 
    /// <summary>
    /// Generates an image index from the given images.
    /// </summary>
    /// <param name="images">Images to generate image index from.</param>
    /// <param name="manifestMediaType">Media type of the manifest.</param>
    /// <param name="imageIndexMediaType">Media type of the produced image index.</param>
    /// <returns>Returns json string of image index and image index mediaType.</returns>
    /// <exception cref="ArgumentException"></exception>
    /// <exception cref="NotSupportedException"></exception>
    internal static string GenerateImageIndex(BuiltImage[] images, string manifestMediaType, string imageIndexMediaType)
    {
        if (images.Length == 0)
        {
            throw new ArgumentException(Strings.ImagesEmpty);
        }
 
        // Here we are using ManifestListV2 struct, but we could use ImageIndexV1 struct as well.
        // We are filling the same fields, so we can use the same struct.
        var manifests = new PlatformSpecificManifest[images.Length];
        
        for (int i = 0; i < images.Length; i++)
        {
            manifests[i] = new PlatformSpecificManifest
            {
                mediaType = manifestMediaType,
                size = images[i].Manifest.Length,
                digest = images[i].ManifestDigest,
                platform = new PlatformInformation
                {
                    architecture = images[i].Architecture!,
                    os = images[i].OS!
                }
            };
        }
 
        var imageIndex = new ManifestListV2
        {
            schemaVersion = 2,
            mediaType = imageIndexMediaType,
            manifests = manifests
        };
 
        return GetJsonStringFromImageIndex(imageIndex);
    }
 
    internal static string GenerateImageIndexWithAnnotations(string manifestMediaType, string manifestDigest, long manifestSize, string repository, string[] tags)
    {
        string containerdImageNamePrefix = repository.Contains('/') ? "docker.io/" : "docker.io/library/";
        
        var manifests = new PlatformSpecificOciManifest[tags.Length];
        for (int i = 0; i < tags.Length; i++)
        {
            var tag = tags[i];
            manifests[i] = new PlatformSpecificOciManifest
            {
                mediaType = manifestMediaType,
                size = manifestSize,
                digest = manifestDigest,
                annotations = new Dictionary<string, string> 
                {
                    { "io.containerd.image.name", $"{containerdImageNamePrefix}{repository}:{tag}" },
                    { "org.opencontainers.image.ref.name", tag } 
                }
            };
        }
 
        var index = new ImageIndexV1
        {
            schemaVersion = 2,
            mediaType = SchemaTypes.OciImageIndexV1,
            manifests = manifests
        };
 
        return GetJsonStringFromImageIndex(index);
    }
 
    private static string GetJsonStringFromImageIndex<T>(T imageIndex)
    {
        var nullIgnoreOptions = new JsonSerializerOptions
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
        // To avoid things like \u002B for '+' especially in media types ("application/vnd.oci.image.manifest.v1\u002Bjson"), we use UnsafeRelaxedJsonEscaping.
        var escapeOptions = new JsonSerializerOptions
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };
 
        return JsonSerializer.SerializeToNode(imageIndex, nullIgnoreOptions)?.ToJsonString(escapeOptions) ?? "";
    }
}