Skip to content

Commit

Permalink
Allow reading of OCI Image Indexes to determine RID-specific base ima…
Browse files Browse the repository at this point in the history
…ge for multi-architecture images (#43631)
  • Loading branch information
baronfel authored Oct 4, 2024
1 parent d92b9fe commit 6ef345b
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ public record struct ManifestListV2(int schemaVersion, string mediaType, Platfor
public record struct PlatformInformation(string architecture, string os, string? variant, string[] features, [property: JsonPropertyName("os.version")][field: JsonPropertyName("os.version")] string? version);

public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform);

public record struct ImageIndexV1(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests);
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ Microsoft.NET.Build.Containers.ManifestListV2.mediaType.get -> string!
Microsoft.NET.Build.Containers.ManifestListV2.mediaType.set -> void
Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.get -> int
Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.set -> void
Microsoft.NET.Build.Containers.ImageIndexV1
Microsoft.NET.Build.Containers.ImageIndexV1.ImageIndexV1() -> void
Microsoft.NET.Build.Containers.ImageIndexV1.ImageIndexV1(int schemaVersion, string! mediaType, Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void
Microsoft.NET.Build.Containers.ImageIndexV1.manifests.get -> Microsoft.NET.Build.Containers.PlatformSpecificManifest[]!
Microsoft.NET.Build.Containers.ImageIndexV1.manifests.set -> void
Microsoft.NET.Build.Containers.ImageIndexV1.mediaType.get -> string!
Microsoft.NET.Build.Containers.ImageIndexV1.mediaType.set -> void
Microsoft.NET.Build.Containers.ImageIndexV1.schemaVersion.get -> int
Microsoft.NET.Build.Containers.ImageIndexV1.schemaVersion.set -> void
Microsoft.NET.Build.Containers.ManifestV2
Microsoft.NET.Build.Containers.ManifestV2.Config.get -> Microsoft.NET.Build.Containers.ManifestConfig
Microsoft.NET.Build.Containers.ManifestV2.Config.init -> void
Expand Down Expand Up @@ -268,4 +277,11 @@ static Microsoft.NET.Build.Containers.ManifestListV2.operator ==(Microsoft.NET.B
override Microsoft.NET.Build.Containers.ManifestListV2.GetHashCode() -> int
~override Microsoft.NET.Build.Containers.ManifestListV2.Equals(object obj) -> bool
Microsoft.NET.Build.Containers.ManifestListV2.Equals(Microsoft.NET.Build.Containers.ManifestListV2 other) -> bool
Microsoft.NET.Build.Containers.ManifestListV2.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void
Microsoft.NET.Build.Containers.ManifestListV2.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void
~override Microsoft.NET.Build.Containers.ImageIndexV1.ToString() -> string
static Microsoft.NET.Build.Containers.ImageIndexV1.operator !=(Microsoft.NET.Build.Containers.ImageIndexV1 left, Microsoft.NET.Build.Containers.ImageIndexV1 right) -> bool
static Microsoft.NET.Build.Containers.ImageIndexV1.operator ==(Microsoft.NET.Build.Containers.ImageIndexV1 left, Microsoft.NET.Build.Containers.ImageIndexV1 right) -> bool
override Microsoft.NET.Build.Containers.ImageIndexV1.GetHashCode() -> int
~override Microsoft.NET.Build.Containers.ImageIndexV1.Equals(object obj) -> bool
Microsoft.NET.Build.Containers.ImageIndexV1.Equals(Microsoft.NET.Build.Containers.ImageIndexV1 other) -> bool
Microsoft.NET.Build.Containers.ImageIndexV1.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
using NuGet.Packaging;

namespace Microsoft.NET.Build.Containers;

internal static class HttpExtensions
{
private static readonly MediaTypeWithQualityHeaderValue[] _knownManifestFormats = [
new("application/json"),
new(SchemaTypes.DockerManifestListV2),
new(SchemaTypes.OciImageIndexV1),
new(SchemaTypes.DockerManifestV2),
new(SchemaTypes.OciManifestV1),
new(SchemaTypes.DockerContainerV1),
];

internal static HttpRequestMessage AcceptManifestFormats(this HttpRequestMessage request)
{
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new("application/json"));
request.Headers.Accept.Add(new(SchemaTypes.DockerManifestListV2));
request.Headers.Accept.Add(new(SchemaTypes.DockerManifestV2));
request.Headers.Accept.Add(new(SchemaTypes.OciManifestV1));
request.Headers.Accept.Add(new(SchemaTypes.DockerContainerV1));
request.Headers.Accept.AddRange(_knownManifestFormats);
return request;
}

Expand Down
59 changes: 52 additions & 7 deletions src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ internal Registry(string registryName, ILogger logger, IRegistryAPI registryAPI,
this(new Uri($"https://{registryName}"), logger, registryAPI, settings)
{ }

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

Expand Down Expand Up @@ -191,6 +191,14 @@ await initialManifestResponse.Content.ReadFromJsonAsync<ManifestListV2>(cancella
runtimeIdentifier,
manifestPicker,
cancellationToken).ConfigureAwait(false),
SchemaTypes.OciImageIndexV1 =>
await PickBestImageFromImageIndexAsync(
repositoryName,
reference,
await initialManifestResponse.Content.ReadFromJsonAsync<ImageIndexV1>(cancellationToken: cancellationToken).ConfigureAwait(false),
runtimeIdentifier,
manifestPicker,
cancellationToken).ConfigureAwait(false),
var unknownMediaType => throw new NotImplementedException(Resource.FormatString(
nameof(Strings.UnknownMediaType),
repositoryName,
Expand Down Expand Up @@ -236,10 +244,10 @@ private async Task<ImageBuilder> ReadSingleImageAsync(string repositoryName, Man
}


private static IReadOnlyDictionary<string, PlatformSpecificManifest> GetManifestsByRid(ManifestListV2 manifestList)
private static IReadOnlyDictionary<string, PlatformSpecificManifest> GetManifestsByRid(PlatformSpecificManifest[] manifestList)
{
var ridDict = new Dictionary<string, PlatformSpecificManifest>();
foreach (var manifest in manifestList.manifests)
foreach (var manifest in manifestList)
{
if (CreateRidForPlatform(manifest.platform) is { } rid)
{
Expand Down Expand Up @@ -293,14 +301,51 @@ private async Task<ImageBuilder> PickBestImageFromManifestListAsync(
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var ridManifestDict = GetManifestsByRid(manifestList);
if (manifestPicker.PickBestManifestForRid(ridManifestDict, runtimeIdentifier) is PlatformSpecificManifest matchingManifest)
var ridManifestDict = GetManifestsByRid(manifestList.manifests);
return await PickBestImageFromManifestsAsync(
repositoryName,
reference,
ridManifestDict,
runtimeIdentifier,
manifestPicker,
cancellationToken).ConfigureAwait(false);
}

private async Task<ImageBuilder> PickBestImageFromImageIndexAsync(
string repositoryName,
string reference,
ImageIndexV1 index,
string runtimeIdentifier,
IManifestPicker manifestPicker,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var ridManifestDict = GetManifestsByRid(index.manifests);
return await PickBestImageFromManifestsAsync(
repositoryName,
reference,
ridManifestDict,
runtimeIdentifier,
manifestPicker,
cancellationToken).ConfigureAwait(false);
}

private async Task<ImageBuilder> PickBestImageFromManifestsAsync(
string repositoryName,
string reference,
IReadOnlyDictionary<string, PlatformSpecificManifest> knownManifests,
string runtimeIdentifier,
IManifestPicker manifestPicker,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (manifestPicker.PickBestManifestForRid(knownManifests, runtimeIdentifier) is PlatformSpecificManifest matchingManifest)
{
using HttpResponseMessage manifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, matchingManifest.digest, cancellationToken).ConfigureAwait(false);

cancellationToken.ThrowIfCancellationRequested();
var manifest = await manifestResponse.Content.ReadFromJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false);
if (manifest is null) throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys);
if (manifest is null) throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, knownManifests.Keys);
manifest.KnownDigest = matchingManifest.digest;
return await ReadSingleImageAsync(
repositoryName,
Expand All @@ -309,7 +354,7 @@ private async Task<ImageBuilder> PickBestImageFromManifestListAsync(
}
else
{
throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys);
throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, knownManifests.Keys);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ internal class SchemaTypes
internal const string DockerManifestListV2 = "application/vnd.docker.distribution.manifest.list.v2+json";
internal const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json";
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
internal const string OciImageIndexV1 = "application/vnd.oci.image.index.v1+json";
internal const string DockerLayerGzip = "application/vnd.docker.image.rootfs.diff.tar.gzip";
internal const string OciLayerGzipV1 = "application/vnd.oci.image.layer.v1.tar+gzip";
}

0 comments on commit 6ef345b

Please sign in to comment.