Skip to content

Commit e78c097

Browse files
authored
[release/8.0.4xx] Multi-arch OCI images export as tarballs (#46467)
2 parents 8103a1c + f09c5c4 commit e78c097

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1825
-847
lines changed

src/Containers/Microsoft.NET.Build.Containers/BuiltImage.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,51 @@ internal readonly struct BuiltImage
1616
/// <summary>
1717
/// Gets image digest.
1818
/// </summary>
19-
internal required string ImageDigest { get; init; }
19+
internal string? ImageDigest { get; init; }
2020

2121
/// <summary>
2222
/// Gets image SHA.
2323
/// </summary>
24-
internal required string ImageSha { get; init; }
24+
internal string? ImageSha { get; init; }
2525

2626
/// <summary>
27-
/// Gets image size.
27+
/// Gets image manifest.
2828
/// </summary>
29-
internal required long ImageSize { get; init; }
29+
internal required string Manifest { get; init; }
3030

3131
/// <summary>
32-
/// Gets image manifest.
32+
/// Gets manifest digest.
3333
/// </summary>
34-
internal required ManifestV2 Manifest { get; init; }
34+
internal required string ManifestDigest { get; init; }
3535

3636
/// <summary>
3737
/// Gets manifest mediaType.
3838
/// </summary>
3939
internal required string ManifestMediaType { get; init; }
4040

41+
/// <summary>
42+
/// Gets image layers.
43+
/// </summary>
44+
internal List<ManifestLayer>? Layers { get; init; }
45+
46+
/// <summary>
47+
/// Gets image OS.
48+
/// </summary>
49+
internal string? OS { get; init; }
50+
51+
/// <summary>
52+
/// Gets image architecture.
53+
/// </summary>
54+
internal string? Architecture { get; init; }
55+
4156
/// <summary>
4257
/// Gets layers descriptors.
4358
/// </summary>
4459
internal IEnumerable<Descriptor> LayerDescriptors
4560
{
4661
get
4762
{
48-
List<ManifestLayer> layersNode = Manifest.Layers ?? throw new NotImplementedException("Tried to get layer information but there is no layer node?");
63+
List<ManifestLayer> layersNode = Layers ?? throw new NotImplementedException("Tried to get layer information but there is no layer node?");
4964
foreach (ManifestLayer layer in layersNode)
5065
{
5166
yield return new(layer.mediaType, layer.digest, layer.size);

src/Containers/Microsoft.NET.Build.Containers/DigestUtils.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ internal sealed class DigestUtils
1717
/// </summary>
1818
internal static string GetDigestFromSha(string sha) => $"sha256:{sha}";
1919

20+
internal static string GetShaFromDigest(string digest)
21+
{
22+
if (!digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
23+
{
24+
throw new ArgumentException($"Invalid digest '{digest}'. Digest must start with 'sha256:'.");
25+
}
26+
27+
return digest.Substring("sha256:".Length);
28+
}
29+
2030
/// <summary>
2131
/// Gets the SHA of <paramref name="str"/>.
2232
/// </summary>

src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.NET.Build.Containers.Resources;
66
using Microsoft.Extensions.Logging;
77
using System.Text.RegularExpressions;
8+
using System.Text.Json;
89

910
namespace Microsoft.NET.Build.Containers;
1011

@@ -86,9 +87,10 @@ internal BuiltImage Build()
8687
Config = imageJsonStr,
8788
ImageDigest = imageDigest,
8889
ImageSha = imageSha,
89-
ImageSize = imageSize,
90-
Manifest = newManifest,
91-
ManifestMediaType = ManifestMediaType
90+
Manifest = JsonSerializer.SerializeToNode(newManifest)?.ToJsonString() ?? "",
91+
ManifestDigest = newManifest.GetDigest(),
92+
ManifestMediaType = ManifestMediaType,
93+
Layers = _manifest.Layers
9294
};
9395
}
9496

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,138 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Text.Encodings.Web;
45
using System.Text.Json;
5-
using System.Text.Json.Nodes;
6+
using System.Text.Json.Serialization;
67
using Microsoft.NET.Build.Containers.Resources;
7-
using Microsoft.NET.Build.Containers.Tasks;
88

99
namespace Microsoft.NET.Build.Containers;
1010

11-
internal readonly struct ImageInfo
12-
{
13-
internal string Config { get; init; }
14-
internal string ManifestDigest { get; init; }
15-
internal string Manifest { get; init; }
16-
internal string ManifestMediaType { get; init; }
17-
18-
public override string ToString() => ManifestDigest;
19-
}
20-
2111
internal static class ImageIndexGenerator
2212
{
2313
/// <summary>
2414
/// Generates an image index from the given images.
2515
/// </summary>
26-
/// <param name="imageInfos"></param>
16+
/// <param name="images">Images to generate image index from.</param>
2717
/// <returns>Returns json string of image index and image index mediaType.</returns>
2818
/// <exception cref="ArgumentException"></exception>
2919
/// <exception cref="NotSupportedException"></exception>
30-
internal static (string, string) GenerateImageIndex(ImageInfo[] imageInfos)
20+
internal static (string, string) GenerateImageIndex(BuiltImage[] images)
3121
{
32-
if (imageInfos.Length == 0)
22+
if (images.Length == 0)
3323
{
34-
throw new ArgumentException(string.Format(Strings.ImagesEmpty));
24+
throw new ArgumentException(Strings.ImagesEmpty);
3525
}
3626

37-
string manifestMediaType = imageInfos[0].ManifestMediaType;
27+
string manifestMediaType = images[0].ManifestMediaType;
3828

39-
if (!imageInfos.All(image => string.Equals(image.ManifestMediaType, manifestMediaType, StringComparison.OrdinalIgnoreCase)))
29+
if (!images.All(image => string.Equals(image.ManifestMediaType, manifestMediaType, StringComparison.OrdinalIgnoreCase)))
4030
{
4131
throw new ArgumentException(Strings.MixedMediaTypes);
4232
}
4333

4434
if (manifestMediaType == SchemaTypes.DockerManifestV2)
4535
{
46-
return GenerateImageIndex(imageInfos, SchemaTypes.DockerManifestV2, SchemaTypes.DockerManifestListV2);
36+
return (GenerateImageIndex(images, SchemaTypes.DockerManifestV2, SchemaTypes.DockerManifestListV2), SchemaTypes.DockerManifestListV2);
4737
}
4838
else if (manifestMediaType == SchemaTypes.OciManifestV1)
4939
{
50-
return GenerateImageIndex(imageInfos, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1);
40+
return (GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1), SchemaTypes.OciImageIndexV1);
5141
}
5242
else
5343
{
5444
throw new NotSupportedException(string.Format(Strings.UnsupportedMediaType, manifestMediaType));
5545
}
5646
}
5747

58-
private static (string, string) GenerateImageIndex(ImageInfo[] images, string manifestMediaType, string imageIndexMediaType)
48+
/// <summary>
49+
/// Generates an image index from the given images.
50+
/// </summary>
51+
/// <param name="images">Images to generate image index from.</param>
52+
/// <param name="manifestMediaType">Media type of the manifest.</param>
53+
/// <param name="imageIndexMediaType">Media type of the produced image index.</param>
54+
/// <returns>Returns json string of image index and image index mediaType.</returns>
55+
/// <exception cref="ArgumentException"></exception>
56+
/// <exception cref="NotSupportedException"></exception>
57+
internal static string GenerateImageIndex(BuiltImage[] images, string manifestMediaType, string imageIndexMediaType)
5958
{
59+
if (images.Length == 0)
60+
{
61+
throw new ArgumentException(Strings.ImagesEmpty);
62+
}
63+
6064
// Here we are using ManifestListV2 struct, but we could use ImageIndexV1 struct as well.
61-
// We are filling the same fiels, so we can use the same struct.
65+
// We are filling the same fields, so we can use the same struct.
6266
var manifests = new PlatformSpecificManifest[images.Length];
67+
6368
for (int i = 0; i < images.Length; i++)
6469
{
65-
var image = images[i];
66-
67-
var manifest = new PlatformSpecificManifest
70+
manifests[i] = new PlatformSpecificManifest
6871
{
6972
mediaType = manifestMediaType,
70-
size = image.Manifest.Length,
71-
digest = image.ManifestDigest,
72-
platform = GetArchitectureAndOsFromConfig(image)
73+
size = images[i].Manifest.Length,
74+
digest = images[i].ManifestDigest,
75+
platform = new PlatformInformation
76+
{
77+
architecture = images[i].Architecture!,
78+
os = images[i].OS!
79+
}
7380
};
74-
manifests[i] = manifest;
7581
}
7682

77-
var dockerManifestList = new ManifestListV2
83+
var imageIndex = new ManifestListV2
7884
{
7985
schemaVersion = 2,
8086
mediaType = imageIndexMediaType,
8187
manifests = manifests
8288
};
8389

84-
return (JsonSerializer.SerializeToNode(dockerManifestList)?.ToJsonString() ?? "", dockerManifestList.mediaType);
90+
return GetJsonStringFromImageIndex(imageIndex);
8591
}
8692

87-
private static PlatformInformation GetArchitectureAndOsFromConfig(ImageInfo image)
93+
internal static string GenerateImageIndexWithAnnotations(string manifestMediaType, string manifestDigest, long manifestSize, string repository, string[] tags)
8894
{
89-
var configJson = JsonNode.Parse(image.Config) as JsonObject ??
90-
throw new ArgumentException($"{nameof(image.Config)} should be a JSON object.", nameof(image.Config));
95+
string containerdImageNamePrefix = repository.Contains('/') ? "docker.io/" : "docker.io/library/";
96+
97+
var manifests = new PlatformSpecificOciManifest[tags.Length];
98+
for (int i = 0; i < tags.Length; i++)
99+
{
100+
var tag = tags[i];
101+
manifests[i] = new PlatformSpecificOciManifest
102+
{
103+
mediaType = manifestMediaType,
104+
size = manifestSize,
105+
digest = manifestDigest,
106+
annotations = new Dictionary<string, string>
107+
{
108+
{ "io.containerd.image.name", $"{containerdImageNamePrefix}{repository}:{tag}" },
109+
{ "org.opencontainers.image.ref.name", tag }
110+
}
111+
};
112+
}
91113

92-
var architecture = configJson["architecture"]?.ToString() ??
93-
throw new ArgumentException($"{nameof(image.Config)} should contain 'architecture'.", nameof(image.Config));
114+
var index = new ImageIndexV1
115+
{
116+
schemaVersion = 2,
117+
mediaType = SchemaTypes.OciImageIndexV1,
118+
manifests = manifests
119+
};
94120

95-
var os = configJson["os"]?.ToString() ??
96-
throw new ArgumentException($"{nameof(image.Config)} should contain 'os'.", nameof(image.Config));
121+
return GetJsonStringFromImageIndex(index);
122+
}
123+
124+
private static string GetJsonStringFromImageIndex<T>(T imageIndex)
125+
{
126+
var nullIgnoreOptions = new JsonSerializerOptions
127+
{
128+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
129+
};
130+
// To avoid things like \u002B for '+' especially in media types ("application/vnd.oci.image.manifest.v1\u002Bjson"), we use UnsafeRelaxedJsonEscaping.
131+
var escapeOptions = new JsonSerializerOptions
132+
{
133+
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
134+
};
97135

98-
return new PlatformInformation { architecture = architecture, os = os };
136+
return JsonSerializer.SerializeToNode(imageIndex, nullIgnoreOptions)?.ToJsonString(escapeOptions) ?? "";
99137
}
100138
}

0 commit comments

Comments
 (0)