Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/8.0.4xx] Multi-arch OCI images export as tarballs #46467

Open
wants to merge 54 commits into
base: release/8.0.4xx
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
81bcbf3
fix e2e test after checking option 'Use containerd for pulling and st…
Jan 28, 2025
01a9c07
refactor BuiltImage
Jan 29, 2025
d973c0e
refactor single-arch oci tarball publishing
Jan 29, 2025
2363f94
implement loading multi-arch image as a tarball and to a local registry
Jan 31, 2025
068d073
implement publishing to tarball/local daemon/remote registry in Creat…
Jan 31, 2025
8d52899
move Telemetry into a seperate file
Jan 31, 2025
0399310
move Telemetry class to another folder
Jan 31, 2025
ec26765
extract publishing image logic into a seperate class
Jan 31, 2025
7acc5e9
refactor ImagePublisher
Jan 31, 2025
181b6fa
skip publishing individual images in case of tarball and local daemon…
Feb 1, 2025
ec09d05
update public api
Feb 1, 2025
5e38e73
skip CreateImageIndex task in case of local damon podman publishing a…
Feb 1, 2025
88c613a
refactor CreateImageIndex task; improve logging; log tip for enabling…
Feb 1, 2025
6b147e1
add support for multi tagoci tarball
Feb 1, 2025
999368f
fix test EndToEndMultiArch_LocalRegistry
Feb 2, 2025
0c9bdd1
check docker availability before arch support in tests
surayya-MS Dec 4, 2024
7b09eb9
refactoring
Feb 2, 2025
6e1035b
fixed multi-arch e2e tests
Feb 2, 2025
9b3fd7e
cleanup
Feb 2, 2025
fb04982
refactoring: make BuiltImage a class instead of a struct
Feb 2, 2025
73b120d
rearrange functions order in DockerCli for easier review
Feb 2, 2025
c8d4e09
rearrange functions again
Feb 2, 2025
f4f1754
add back log message about building image index
Feb 2, 2025
fc80348
extract error messages into Strings
Feb 2, 2025
f2909c1
fix typo in log message
Feb 3, 2025
a61aad5
extract image index creation from DockerCli into ImageIndexGenerator
Feb 3, 2025
51d9cc8
cleanup
Feb 3, 2025
a0c3a56
add more ImageIndexGenertorTests
Feb 3, 2025
3078ea8
check if containerd store is enabled before loading
Feb 3, 2025
af3a548
cleanup targets
Feb 5, 2025
69026ea
add docker.io/library prefix to image name annotation; fix json seria…
Feb 5, 2025
a821ac2
delete empty lines
Feb 5, 2025
8bea2ec
fix EndToEndMultiArch_ArchivePublishing test
Feb 5, 2025
2e6adab
update EndToEndMultiArch_RemoteRegistry test since containerd store i…
Feb 5, 2025
929c2b3
go back to running containers by image and not imageId for multi-arch…
Feb 5, 2025
e46389b
implement skipping tests if containerd image store is not enabled
Feb 5, 2025
299e6d1
remove using
Feb 5, 2025
2c1705b
Merge branch 'release/8.0.4xx' into multi-arch-tarballs
surayya-MS Feb 5, 2025
674ee0e
refactor to bring back GeneratedImageIndex; fix ImageIndexGeneratorTests
Feb 5, 2025
adaffdd
small fix
Feb 5, 2025
757a0ef
Merge remote-tracking branch 'origin/multi-arch-tarballs' into multi-…
Feb 5, 2025
3285270
make BuiltImage and MultiArchImage readonly structs
Feb 6, 2025
aef3546
Merge branch 'release/8.0.4xx' into multi-arch-tarballs
surayya-MS Feb 6, 2025
9cea231
if ArchiveOutputPath does't have file extension then treat it as dire…
Feb 6, 2025
d998510
trim BaseRegistry before checking null or empty
Feb 6, 2025
5094a41
BaseRegistry shouldn't be required parameter
Feb 6, 2025
e9f4af3
address comments
Feb 8, 2025
65837f6
refactor ImagePublisher
Feb 8, 2025
960bb7d
remove BuildEngine null check before logging
Feb 8, 2025
16b98ae
Merge branch 'release/8.0.4xx' into multi-arch-tarballs
surayya-MS Feb 8, 2025
63847ba
register task resources for CreateImageIndex
Feb 8, 2025
72b4bf0
optimize ParseImages in CreateImageIndex
Feb 10, 2025
0f5e6c8
fix setting annotation image name prefix: set to 'docker.io/' when re…
Feb 10, 2025
f09c5c4
move DockerIsAvailableAndSupportsArchTheoryAttribute to its own file;…
Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/Containers/Microsoft.NET.Build.Containers/BuiltImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,43 @@ internal readonly struct BuiltImage
internal required string ImageSha { get; init; }

/// <summary>
/// Gets image size.
/// Gets image manifest.
/// </summary>
internal required long ImageSize { get; init; }
internal required string Manifest { get; init; }

/// <summary>
/// Gets image manifest.
/// Gets manifest digest.
/// </summary>
internal required ManifestV2 Manifest { get; init; }
internal required string ManifestDigest { get; init; }

/// <summary>
/// Gets manifest mediaType.
/// </summary>
internal required string ManifestMediaType { get; init; }

/// <summary>
/// Gets image layers.
/// </summary>
internal List<ManifestLayer>? Layers { get; init; }

/// <summary>
/// Gets image OS.
/// </summary>
internal string? OS { get; init; }

/// <summary>
/// Gets image architecture.
/// </summary>
internal string? Architecture { get; init; }

/// <summary>
/// Gets layers descriptors.
/// </summary>
internal IEnumerable<Descriptor> LayerDescriptors
{
get
{
List<ManifestLayer> layersNode = Manifest.Layers ?? throw new NotImplementedException("Tried to get layer information but there is no layer node?");
List<ManifestLayer> layersNode = Layers ?? throw new NotImplementedException("Tried to get layer information but there is no layer node?");
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
foreach (ManifestLayer layer in layersNode)
{
yield return new(layer.mediaType, layer.digest, layer.size);
Expand Down
10 changes: 10 additions & 0 deletions src/Containers/Microsoft.NET.Build.Containers/DigestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ internal sealed class DigestUtils
/// </summary>
internal static string GetDigestFromSha(string sha) => $"sha256:{sha}";

internal static string GetShaFromDigest(string digest)
{
if (!digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Invalid digest format. Digest must start with 'sha256:'.");
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
}

return digest.Substring("sha256:".Length);
}

/// <summary>
/// Gets the SHA of <paramref name="str"/>.
/// </summary>
Expand Down
8 changes: 5 additions & 3 deletions src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.NET.Build.Containers.Resources;
using Microsoft.Extensions.Logging;
using System.Text.RegularExpressions;
using System.Text.Json;

namespace Microsoft.NET.Build.Containers;

Expand Down Expand Up @@ -86,9 +87,10 @@ internal BuiltImage Build()
Config = imageJsonStr,
ImageDigest = imageDigest,
ImageSha = imageSha,
ImageSize = imageSize,
Manifest = newManifest,
ManifestMediaType = ManifestMediaType
Manifest = JsonSerializer.SerializeToNode(newManifest)?.ToJsonString() ?? "",
ManifestDigest = newManifest.GetDigest(),
ManifestMediaType = ManifestMediaType,
Layers = _manifest.Layers
};
}

Expand Down
104 changes: 65 additions & 39 deletions src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,100 +1,126 @@
// 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.Nodes;
using System.Text.Json.Serialization;
using Microsoft.NET.Build.Containers.Resources;
using Microsoft.NET.Build.Containers.Tasks;

namespace Microsoft.NET.Build.Containers;

internal readonly struct ImageInfo
{
internal string Config { get; init; }
internal string ManifestDigest { get; init; }
internal string Manifest { get; init; }
internal string ManifestMediaType { get; init; }

public override string ToString() => ManifestDigest;
}

internal static class ImageIndexGenerator
{
/// <summary>
/// Generates an image index from the given images.
/// </summary>
/// <param name="imageInfos"></param>
/// <param name="images"></param>
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
/// <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(ImageInfo[] imageInfos)
internal static (string, string) GenerateImageIndex(BuiltImage[] images)
{
if (imageInfos.Length == 0)
if (images.Length == 0)
{
throw new ArgumentException(string.Format(Strings.ImagesEmpty));
throw new ArgumentException(Strings.ImagesEmpty);
}

string manifestMediaType = imageInfos[0].ManifestMediaType;
string manifestMediaType = images[0].ManifestMediaType;

if (!imageInfos.All(image => string.Equals(image.ManifestMediaType, manifestMediaType, StringComparison.OrdinalIgnoreCase)))
if (!images.All(image => string.Equals(image.ManifestMediaType, manifestMediaType, StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException(Strings.MixedMediaTypes);
}

if (manifestMediaType == SchemaTypes.DockerManifestV2)
{
return GenerateImageIndex(imageInfos, SchemaTypes.DockerManifestV2, SchemaTypes.DockerManifestListV2);
return (GenerateImageIndex(images, SchemaTypes.DockerManifestV2, SchemaTypes.DockerManifestListV2), SchemaTypes.DockerManifestListV2);
}
else if (manifestMediaType == SchemaTypes.OciManifestV1)
{
return GenerateImageIndex(imageInfos, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1);
return (GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1), SchemaTypes.OciImageIndexV1);
}
else
{
throw new NotSupportedException(string.Format(Strings.UnsupportedMediaType, manifestMediaType));
}
}

private static (string, string) GenerateImageIndex(ImageInfo[] images, string manifestMediaType, string imageIndexMediaType)
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 fiels, so we can use the same struct.
// 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++)
{
var image = images[i];

var manifest = new PlatformSpecificManifest
manifests[i] = new PlatformSpecificManifest
{
mediaType = manifestMediaType,
size = image.Manifest.Length,
digest = image.ManifestDigest,
platform = GetArchitectureAndOsFromConfig(image)
size = images[i].Manifest.Length,
digest = images[i].ManifestDigest,
platform = new PlatformInformation
{
architecture = images[i].Architecture!,
os = images[i].OS!
}
};
manifests[i] = manifest;
}

var dockerManifestList = new ManifestListV2
var imageIndex = new ManifestListV2
{
schemaVersion = 2,
mediaType = imageIndexMediaType,
manifests = manifests
};

return (JsonSerializer.SerializeToNode(dockerManifestList)?.ToJsonString() ?? "", dockerManifestList.mediaType);
return GetJsonStringFromImageIndex(imageIndex);
}

private static PlatformInformation GetArchitectureAndOsFromConfig(ImageInfo image)
internal static string GenerateImageIndexWithAnnotations(string manifestMediaType, string manifestDigest, long manifestSize, string repository, string[] tags)
{
var configJson = JsonNode.Parse(image.Config) as JsonObject ??
throw new ArgumentException($"{nameof(image.Config)} should be a JSON object.", nameof(image.Config));
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", $"docker.io/library/{repository}:{tag}" },
{ "org.opencontainers.image.ref.name", tag }
}
};
}

var index = new ImageIndexV1
{
schemaVersion = 2,
mediaType = SchemaTypes.OciImageIndexV1,
manifests = manifests
};

var architecture = configJson["architecture"]?.ToString() ??
throw new ArgumentException($"{nameof(image.Config)} should contain 'architecture'.", nameof(image.Config));
return GetJsonStringFromImageIndex(index);
}

var os = configJson["os"]?.ToString() ??
throw new ArgumentException($"{nameof(image.Config)} should contain 'os'.", nameof(image.Config));
private static string GetJsonStringFromImageIndex<T>(T imageIndex)
{
var nullIgnoreOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var escapeOptions = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
};

return new PlatformInformation { architecture = architecture, os = os };
return JsonSerializer.SerializeToNode(imageIndex, nullIgnoreOptions)?.ToJsonString(escapeOptions) ?? "";
}
}
Loading
Loading