Skip to content

Commit 3d57238

Browse files
[release/8.0.4xx] Allow reading of OCI Image Indexes to determine RID-specific base image for multi-architecture images (#43914)
Co-authored-by: Chet Husk <[email protected]>
1 parent ba7c2e6 commit 3d57238

File tree

5 files changed

+84
-14
lines changed

5 files changed

+84
-14
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ public record struct ManifestListV2(int schemaVersion, string mediaType, Platfor
1010
public record struct PlatformInformation(string architecture, string os, string? variant, string[] features, [property: JsonPropertyName("os.version")][field: JsonPropertyName("os.version")] string? version);
1111

1212
public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform);
13+
14+
public record struct ImageIndexV1(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests);

src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ Microsoft.NET.Build.Containers.ManifestListV2.mediaType.get -> string!
7272
Microsoft.NET.Build.Containers.ManifestListV2.mediaType.set -> void
7373
Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.get -> int
7474
Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.set -> void
75+
Microsoft.NET.Build.Containers.ImageIndexV1
76+
Microsoft.NET.Build.Containers.ImageIndexV1.ImageIndexV1() -> void
77+
Microsoft.NET.Build.Containers.ImageIndexV1.ImageIndexV1(int schemaVersion, string! mediaType, Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void
78+
Microsoft.NET.Build.Containers.ImageIndexV1.manifests.get -> Microsoft.NET.Build.Containers.PlatformSpecificManifest[]!
79+
Microsoft.NET.Build.Containers.ImageIndexV1.manifests.set -> void
80+
Microsoft.NET.Build.Containers.ImageIndexV1.mediaType.get -> string!
81+
Microsoft.NET.Build.Containers.ImageIndexV1.mediaType.set -> void
82+
Microsoft.NET.Build.Containers.ImageIndexV1.schemaVersion.get -> int
83+
Microsoft.NET.Build.Containers.ImageIndexV1.schemaVersion.set -> void
7584
Microsoft.NET.Build.Containers.ManifestV2
7685
Microsoft.NET.Build.Containers.ManifestV2.Config.get -> Microsoft.NET.Build.Containers.ManifestConfig
7786
Microsoft.NET.Build.Containers.ManifestV2.Config.init -> void
@@ -268,4 +277,11 @@ static Microsoft.NET.Build.Containers.ManifestListV2.operator ==(Microsoft.NET.B
268277
override Microsoft.NET.Build.Containers.ManifestListV2.GetHashCode() -> int
269278
~override Microsoft.NET.Build.Containers.ManifestListV2.Equals(object obj) -> bool
270279
Microsoft.NET.Build.Containers.ManifestListV2.Equals(Microsoft.NET.Build.Containers.ManifestListV2 other) -> bool
271-
Microsoft.NET.Build.Containers.ManifestListV2.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void
280+
Microsoft.NET.Build.Containers.ManifestListV2.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void
281+
~override Microsoft.NET.Build.Containers.ImageIndexV1.ToString() -> string
282+
static Microsoft.NET.Build.Containers.ImageIndexV1.operator !=(Microsoft.NET.Build.Containers.ImageIndexV1 left, Microsoft.NET.Build.Containers.ImageIndexV1 right) -> bool
283+
static Microsoft.NET.Build.Containers.ImageIndexV1.operator ==(Microsoft.NET.Build.Containers.ImageIndexV1 left, Microsoft.NET.Build.Containers.ImageIndexV1 right) -> bool
284+
override Microsoft.NET.Build.Containers.ImageIndexV1.GetHashCode() -> int
285+
~override Microsoft.NET.Build.Containers.ImageIndexV1.Equals(object obj) -> bool
286+
Microsoft.NET.Build.Containers.ImageIndexV1.Equals(Microsoft.NET.Build.Containers.ImageIndexV1 other) -> bool
287+
Microsoft.NET.Build.Containers.ImageIndexV1.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void

src/Containers/Microsoft.NET.Build.Containers/Registry/HttpExtensions.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3-
//
43

4+
using System.Net.Http.Headers;
55
using Microsoft.Extensions.Logging;
6+
using NuGet.Packaging;
67

78
namespace Microsoft.NET.Build.Containers;
89

910
internal static class HttpExtensions
1011
{
12+
private static readonly MediaTypeWithQualityHeaderValue[] _knownManifestFormats = [
13+
new("application/json"),
14+
new(SchemaTypes.DockerManifestListV2),
15+
new(SchemaTypes.OciImageIndexV1),
16+
new(SchemaTypes.DockerManifestV2),
17+
new(SchemaTypes.OciManifestV1),
18+
new(SchemaTypes.DockerContainerV1),
19+
];
20+
1121
internal static HttpRequestMessage AcceptManifestFormats(this HttpRequestMessage request)
1222
{
1323
request.Headers.Accept.Clear();
14-
request.Headers.Accept.Add(new("application/json"));
15-
request.Headers.Accept.Add(new(SchemaTypes.DockerManifestListV2));
16-
request.Headers.Accept.Add(new(SchemaTypes.DockerManifestV2));
17-
request.Headers.Accept.Add(new(SchemaTypes.OciManifestV1));
18-
request.Headers.Accept.Add(new(SchemaTypes.DockerContainerV1));
24+
request.Headers.Accept.AddRange(_knownManifestFormats);
1925
return request;
2026
}
2127

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

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ internal Registry(string registryName, ILogger logger, IRegistryAPI registryAPI,
8181
this(new Uri($"https://{registryName}"), logger, registryAPI, settings)
8282
{ }
8383

84-
internal Registry(string registryName, ILogger logger, RegistryMode mode, RegistrySettings? settings = null) :
84+
internal Registry(string registryName, ILogger logger, RegistryMode mode, RegistrySettings? settings = null) :
8585
this(new Uri($"https://{registryName}"), logger, new RegistryApiFactory(mode), settings)
8686
{ }
8787

@@ -191,6 +191,14 @@ await initialManifestResponse.Content.ReadFromJsonAsync<ManifestListV2>(cancella
191191
runtimeIdentifier,
192192
manifestPicker,
193193
cancellationToken).ConfigureAwait(false),
194+
SchemaTypes.OciImageIndexV1 =>
195+
await PickBestImageFromImageIndexAsync(
196+
repositoryName,
197+
reference,
198+
await initialManifestResponse.Content.ReadFromJsonAsync<ImageIndexV1>(cancellationToken: cancellationToken).ConfigureAwait(false),
199+
runtimeIdentifier,
200+
manifestPicker,
201+
cancellationToken).ConfigureAwait(false),
194202
var unknownMediaType => throw new NotImplementedException(Resource.FormatString(
195203
nameof(Strings.UnknownMediaType),
196204
repositoryName,
@@ -236,10 +244,10 @@ private async Task<ImageBuilder> ReadSingleImageAsync(string repositoryName, Man
236244
}
237245

238246

239-
private static IReadOnlyDictionary<string, PlatformSpecificManifest> GetManifestsByRid(ManifestListV2 manifestList)
247+
private static IReadOnlyDictionary<string, PlatformSpecificManifest> GetManifestsByRid(PlatformSpecificManifest[] manifestList)
240248
{
241249
var ridDict = new Dictionary<string, PlatformSpecificManifest>();
242-
foreach (var manifest in manifestList.manifests)
250+
foreach (var manifest in manifestList)
243251
{
244252
if (CreateRidForPlatform(manifest.platform) is { } rid)
245253
{
@@ -291,14 +299,51 @@ private async Task<ImageBuilder> PickBestImageFromManifestListAsync(
291299
CancellationToken cancellationToken)
292300
{
293301
cancellationToken.ThrowIfCancellationRequested();
294-
var ridManifestDict = GetManifestsByRid(manifestList);
295-
if (manifestPicker.PickBestManifestForRid(ridManifestDict, runtimeIdentifier) is PlatformSpecificManifest matchingManifest)
302+
var ridManifestDict = GetManifestsByRid(manifestList.manifests);
303+
return await PickBestImageFromManifestsAsync(
304+
repositoryName,
305+
reference,
306+
ridManifestDict,
307+
runtimeIdentifier,
308+
manifestPicker,
309+
cancellationToken).ConfigureAwait(false);
310+
}
311+
312+
private async Task<ImageBuilder> PickBestImageFromImageIndexAsync(
313+
string repositoryName,
314+
string reference,
315+
ImageIndexV1 index,
316+
string runtimeIdentifier,
317+
IManifestPicker manifestPicker,
318+
CancellationToken cancellationToken)
319+
{
320+
cancellationToken.ThrowIfCancellationRequested();
321+
var ridManifestDict = GetManifestsByRid(index.manifests);
322+
return await PickBestImageFromManifestsAsync(
323+
repositoryName,
324+
reference,
325+
ridManifestDict,
326+
runtimeIdentifier,
327+
manifestPicker,
328+
cancellationToken).ConfigureAwait(false);
329+
}
330+
331+
private async Task<ImageBuilder> PickBestImageFromManifestsAsync(
332+
string repositoryName,
333+
string reference,
334+
IReadOnlyDictionary<string, PlatformSpecificManifest> knownManifests,
335+
string runtimeIdentifier,
336+
IManifestPicker manifestPicker,
337+
CancellationToken cancellationToken)
338+
{
339+
cancellationToken.ThrowIfCancellationRequested();
340+
if (manifestPicker.PickBestManifestForRid(knownManifests, runtimeIdentifier) is PlatformSpecificManifest matchingManifest)
296341
{
297342
using HttpResponseMessage manifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, matchingManifest.digest, cancellationToken).ConfigureAwait(false);
298343

299344
cancellationToken.ThrowIfCancellationRequested();
300345
var manifest = await manifestResponse.Content.ReadFromJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false);
301-
if (manifest is null) throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys);
346+
if (manifest is null) throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, knownManifests.Keys);
302347
manifest.KnownDigest = matchingManifest.digest;
303348
return await ReadSingleImageAsync(
304349
repositoryName,
@@ -307,7 +352,7 @@ private async Task<ImageBuilder> PickBestImageFromManifestListAsync(
307352
}
308353
else
309354
{
310-
throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys);
355+
throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, knownManifests.Keys);
311356
}
312357
}
313358

src/Containers/Microsoft.NET.Build.Containers/Registry/SchemaTypes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal class SchemaTypes
1010
internal const string DockerManifestListV2 = "application/vnd.docker.distribution.manifest.list.v2+json";
1111
internal const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json";
1212
internal const string OciManifestV1 = "application/vnd.oci.image.manifest.v1+json"; // https://containers.gitbook.io/build-containers-the-hard-way/#registry-format-oci-image-manifest
13+
internal const string OciImageIndexV1 = "application/vnd.oci.image.index.v1+json";
1314
internal const string DockerLayerGzip = "application/vnd.docker.image.rootfs.diff.tar.gzip";
1415
internal const string OciLayerGzipV1 = "application/vnd.oci.image.layer.v1.tar+gzip";
1516
}

0 commit comments

Comments
 (0)