Skip to content

Commit 1adff27

Browse files
committed
detect and flow through the manifest digest from the registry or manifest list
1 parent abac232 commit 1adff27

File tree

4 files changed

+55
-29
lines changed

4 files changed

+55
-29
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ namespace Microsoft.NET.Build.Containers;
1414
/// </summary>
1515
internal sealed class ImageBuilder
1616
{
17+
// a snapshot of the manifest that this builder is based on
18+
private readonly ManifestV2 _baseImageManifest;
19+
20+
// the mutable internal manifest that we're building by modifying the base and applying customizations
1721
private readonly ManifestV2 _manifest;
1822
private readonly ImageConfig _baseImageConfig;
1923
private readonly ILogger _logger;
@@ -33,7 +37,8 @@ internal sealed class ImageBuilder
3337

3438
internal ImageBuilder(ManifestV2 manifest, ImageConfig baseImageConfig, ILogger logger)
3539
{
36-
_manifest = manifest;
40+
_baseImageManifest = manifest;
41+
_manifest = new ManifestV2() { SchemaVersion = manifest.SchemaVersion, Config = manifest.Config, Layers = new(manifest.Layers), MediaType = manifest.MediaType };
3742
_baseImageConfig = baseImageConfig;
3843
_logger = logger;
3944
}
@@ -63,9 +68,12 @@ internal BuiltImage Build()
6368
size = imageSize
6469
};
6570

66-
ManifestV2 newManifest = _manifest with
71+
ManifestV2 newManifest = new ManifestV2()
6772
{
68-
Config = newManifestConfig
73+
Config = newManifestConfig,
74+
SchemaVersion = _manifest.SchemaVersion,
75+
MediaType = _manifest.MediaType,
76+
Layers = _manifest.Layers
6977
};
7078

7179
return new BuiltImage()
@@ -89,7 +97,7 @@ internal void AddLayer(Layer l)
8997

9098
internal void AddBaseImageDigestLabel()
9199
{
92-
AddLabel("org.opencontainers.image.base.digest", _manifest.GetDigest());
100+
AddLabel("org.opencontainers.image.base.digest", _baseImageManifest.GetDigest());
93101
}
94102

95103
/// <summary>

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ namespace Microsoft.NET.Build.Containers;
1212
/// <remarks>
1313
/// https://github.com/opencontainers/image-spec/blob/main/manifest.md
1414
/// </remarks>
15-
public readonly record struct ManifestV2
15+
public class ManifestV2
1616
{
17+
[JsonIgnore]
18+
public string? KnownDigest { get; set; }
19+
1720
/// <summary>
1821
/// This REQUIRED property specifies the image manifest schema version.
1922
/// For this version of the specification, this MUST be 2 to ensure backward compatibility with older versions of Docker.
@@ -47,9 +50,9 @@ public readonly record struct ManifestV2
4750
/// <summary>
4851
/// Gets the digest for this manifest.
4952
/// </summary>
50-
public string GetDigest() => DigestUtils.GetDigest(JsonSerializer.SerializeToNode(this)?.ToJsonString() ?? string.Empty);
53+
public string GetDigest() => KnownDigest ??= DigestUtils.GetDigest(JsonSerializer.SerializeToNode(this)?.ToJsonString(new JsonSerializerOptions() { WriteIndented = true }).ReplaceLineEndings("\r") ?? string.Empty);
5154
}
5255

5356
public record struct ManifestConfig(string mediaType, long size, string digest);
5457

55-
public record struct ManifestLayer(string mediaType, long size, string digest, string[]? urls);
58+
public record struct ManifestLayer(string mediaType, long size, string digest, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)][field: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string[]? urls);

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ Microsoft.NET.Build.Containers.ManifestV2
7373
Microsoft.NET.Build.Containers.ManifestV2.Config.get -> Microsoft.NET.Build.Containers.ManifestConfig
7474
Microsoft.NET.Build.Containers.ManifestV2.Config.init -> void
7575
Microsoft.NET.Build.Containers.ManifestV2.GetDigest() -> string!
76+
Microsoft.NET.Build.Containers.ManifestV2.KnownDigest.get -> string?
77+
Microsoft.NET.Build.Containers.ManifestV2.KnownDigest.set -> void
7678
Microsoft.NET.Build.Containers.ManifestV2.Layers.get -> System.Collections.Generic.List<Microsoft.NET.Build.Containers.ManifestLayer>!
7779
Microsoft.NET.Build.Containers.ManifestV2.Layers.init -> void
7880
Microsoft.NET.Build.Containers.ManifestV2.ManifestV2() -> void
@@ -256,12 +258,6 @@ Microsoft.NET.Build.Containers.ManifestLayer.Equals(Microsoft.NET.Build.Containe
256258
~override Microsoft.NET.Build.Containers.Descriptor.Equals(object obj) -> bool
257259
Microsoft.NET.Build.Containers.ManifestLayer.Deconstruct(out string! mediaType, out long size, out string! digest, out string![]? urls) -> void
258260
Microsoft.NET.Build.Containers.Descriptor.Equals(Microsoft.NET.Build.Containers.Descriptor other) -> bool
259-
~override Microsoft.NET.Build.Containers.ManifestV2.ToString() -> string
260-
static Microsoft.NET.Build.Containers.ManifestV2.operator !=(Microsoft.NET.Build.Containers.ManifestV2 left, Microsoft.NET.Build.Containers.ManifestV2 right) -> bool
261-
static Microsoft.NET.Build.Containers.ManifestV2.operator ==(Microsoft.NET.Build.Containers.ManifestV2 left, Microsoft.NET.Build.Containers.ManifestV2 right) -> bool
262-
override Microsoft.NET.Build.Containers.ManifestV2.GetHashCode() -> int
263-
~override Microsoft.NET.Build.Containers.ManifestV2.Equals(object obj) -> bool
264-
Microsoft.NET.Build.Containers.ManifestV2.Equals(Microsoft.NET.Build.Containers.ManifestV2 other) -> bool
265261
~override Microsoft.NET.Build.Containers.ManifestListV2.ToString() -> string
266262
static Microsoft.NET.Build.Containers.ManifestListV2.operator !=(Microsoft.NET.Build.Containers.ManifestListV2 left, Microsoft.NET.Build.Containers.ManifestListV2 right) -> bool
267263
static Microsoft.NET.Build.Containers.ManifestListV2.operator ==(Microsoft.NET.Build.Containers.ManifestListV2 left, Microsoft.NET.Build.Containers.ManifestListV2 right) -> bool

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

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
using NuGet.RuntimeModel;
77
using System.Diagnostics;
88
using System.Net.Http.Json;
9+
using System.Text.Json;
910
using System.Text.Json.Nodes;
1011
using System.Text.RegularExpressions;
1112

1213
namespace Microsoft.NET.Build.Containers;
1314

14-
internal interface IManifestPicker {
15+
internal interface IManifestPicker
16+
{
1517
public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary<string, PlatformSpecificManifest> manifestList, string runtimeIdentifier);
1618
}
1719

@@ -26,7 +28,8 @@ public RidGraphManifestPicker(string runtimeIdentifierGraphPath)
2628
public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary<string, PlatformSpecificManifest> ridManifestDict, string runtimeIdentifier)
2729
{
2830
var bestManifestRid = GetBestMatchingRid(_runtimeGraph, runtimeIdentifier, ridManifestDict.Keys);
29-
if (bestManifestRid is null) {
31+
if (bestManifestRid is null)
32+
{
3033
return null;
3134
}
3235
return ridManifestDict[bestManifestRid];
@@ -132,7 +135,8 @@ private static string DeriveRegistryName(Uri baseUri)
132135
/// <remarks>
133136
/// Google Artifact Registry locations (one for each availability zone) are of the form "ZONE-docker.pkg.dev".
134137
/// </remarks>
135-
public bool IsGoogleArtifactRegistry {
138+
public bool IsGoogleArtifactRegistry
139+
{
136140
get => RegistryName.EndsWith("-docker.pkg.dev", StringComparison.Ordinal);
137141
}
138142

@@ -151,7 +155,7 @@ public async Task<ImageBuilder> GetImageManifestAsync(string repositoryName, str
151155
{
152156
SchemaTypes.DockerManifestV2 or SchemaTypes.OciManifestV1 => await ReadSingleImageAsync(
153157
repositoryName,
154-
await initialManifestResponse.Content.ReadFromJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false),
158+
await ReadManifest().ConfigureAwait(false),
155159
cancellationToken).ConfigureAwait(false),
156160
SchemaTypes.DockerManifestListV2 => await PickBestImageFromManifestListAsync(
157161
repositoryName,
@@ -167,6 +171,17 @@ await initialManifestResponse.Content.ReadFromJsonAsync<ManifestListV2>(cancella
167171
BaseUri,
168172
unknownMediaType))
169173
};
174+
175+
async Task<ManifestV2> ReadManifest()
176+
{
177+
initialManifestResponse.Headers.TryGetValues("Docker-Content-Digest", out var knownDigest);
178+
var manifest = (await initialManifestResponse.Content.ReadFromJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false))!;
179+
if (knownDigest?.FirstOrDefault() is string knownDigestValue)
180+
{
181+
manifest.KnownDigest = knownDigestValue;
182+
}
183+
return manifest;
184+
}
170185
}
171186

172187
internal async Task<ManifestListV2?> GetManifestListAsync(string repositoryName, string reference, CancellationToken cancellationToken)
@@ -193,11 +208,12 @@ private async Task<ImageBuilder> ReadSingleImageAsync(string repositoryName, Man
193208
return new ImageBuilder(manifest, new ImageConfig(configDoc), _logger);
194209
}
195210

196-
211+
197212
private static IReadOnlyDictionary<string, PlatformSpecificManifest> GetManifestsByRid(ManifestListV2 manifestList)
198213
{
199214
var ridDict = new Dictionary<string, PlatformSpecificManifest>();
200-
foreach (var manifest in manifestList.manifests) {
215+
foreach (var manifest in manifestList.manifests)
216+
{
201217
if (CreateRidForPlatform(manifest.platform) is { } rid)
202218
{
203219
ridDict.TryAdd(rid, manifest);
@@ -206,7 +222,7 @@ private static IReadOnlyDictionary<string, PlatformSpecificManifest> GetManifest
206222

207223
return ridDict;
208224
}
209-
225+
210226
private static string? CreateRidForPlatform(PlatformInformation platform)
211227
{
212228
// we only support linux and windows containers explicitly, so anything else we should skip past.
@@ -220,7 +236,7 @@ private static IReadOnlyDictionary<string, PlatformSpecificManifest> GetManifest
220236
// TODO: we _may_ need OS-specific version parsing. Need to do more research on what the field looks like across more manifest lists.
221237
var versionPart = platform.version?.Split('.') switch
222238
{
223-
[var major, .. ] => major,
239+
[var major, ..] => major,
224240
_ => null
225241
};
226242
var platformPart = platform.architecture switch
@@ -254,12 +270,14 @@ private async Task<ImageBuilder> PickBestImageFromManifestListAsync(
254270
using HttpResponseMessage manifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, matchingManifest.digest, cancellationToken).ConfigureAwait(false);
255271

256272
cancellationToken.ThrowIfCancellationRequested();
257-
273+
var manifest = await manifestResponse.Content.ReadAsJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false);
274+
manifest.KnownDigest = matchingManifest.digest;
258275
return await ReadSingleImageAsync(
259276
repositoryName,
260-
await manifestResponse.Content.ReadFromJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false),
277+
manifest,
261278
cancellationToken).ConfigureAwait(false);
262-
} else
279+
}
280+
else
263281
{
264282
throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys);
265283
}
@@ -332,13 +350,13 @@ internal async Task<FinalizeUploadInformation> UploadBlobChunkedAsync(Stream con
332350

333351
int bytesRead = await contents.ReadAsync(chunkBackingStore, cancellationToken).ConfigureAwait(false);
334352

335-
ByteArrayContent content = new (chunkBackingStore, offset: 0, count: bytesRead);
353+
ByteArrayContent content = new(chunkBackingStore, offset: 0, count: bytesRead);
336354
content.Headers.ContentLength = bytesRead;
337355

338356
// manual because ACR throws an error with the .NET type {"Range":"bytes 0-84521/*","Reason":"the Content-Range header format is invalid"}
339357
// content.Headers.Add("Content-Range", $"0-{contents.Length - 1}");
340358
Debug.Assert(content.Headers.TryAddWithoutValidation("Content-Range", $"{chunkStart}-{chunkStart + bytesRead - 1}"));
341-
359+
342360
NextChunkUploadInformation nextChunk = await _registryAPI.Blob.Upload.UploadChunkAsync(patchUri, content, cancellationToken).ConfigureAwait(false);
343361
patchUri = nextChunk.UploadUri;
344362

@@ -420,7 +438,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source,
420438
}
421439

422440
// Blob wasn't there; can we tell the server to get it from the base image?
423-
if (! await _registryAPI.Blob.Upload.TryMountAsync(destination.Repository, source.Repository, digest, cancellationToken).ConfigureAwait(false))
441+
if (!await _registryAPI.Blob.Upload.TryMountAsync(destination.Repository, source.Repository, digest, cancellationToken).ConfigureAwait(false))
424442
{
425443
// The blob wasn't already available in another namespace, so fall back to explicitly uploading it
426444

@@ -432,7 +450,8 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source,
432450
await destinationRegistry.PushLayerAsync(Layer.FromDescriptor(descriptor), destination.Repository, cancellationToken).ConfigureAwait(false);
433451
_logger.LogInformation(Strings.Registry_LayerUploaded, digest, destinationRegistry.RegistryName);
434452
}
435-
else {
453+
else
454+
{
436455
throw new NotImplementedException(Resource.GetString(nameof(Strings.MissingLinkToRegistry)));
437456
}
438457
}
@@ -444,7 +463,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source,
444463
}
445464
else
446465
{
447-
foreach(var descriptor in builtImage.LayerDescriptors)
466+
foreach (var descriptor in builtImage.LayerDescriptors)
448467
{
449468
await uploadLayerFunc(descriptor).ConfigureAwait(false);
450469
}

0 commit comments

Comments
 (0)