Skip to content

Commit 1c25e55

Browse files
authored
Add an image digest label to generated containers (#39160)
2 parents e33d9e6 + c55692c commit 1c25e55

29 files changed

+395
-95
lines changed

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
namespace Microsoft.NET.Build.Containers;
88

9-
public static class ContainerBuilder
9+
internal static class ContainerBuilder
1010
{
11-
public static async Task<int> ContainerizeAsync(
11+
internal static async Task<int> ContainerizeAsync(
1212
DirectoryInfo publishDirectory,
1313
string workingDir,
1414
string baseRegistry,
@@ -31,6 +31,8 @@ public static async Task<int> ContainerizeAsync(
3131
string localRegistry,
3232
string? containerUser,
3333
string? archiveOutputPath,
34+
bool generateLabels,
35+
bool generateDigestLabel,
3436
ILoggerFactory loggerFactory,
3537
CancellationToken cancellationToken)
3638
{
@@ -124,11 +126,20 @@ public static async Task<int> ContainerizeAsync(
124126
}
125127
imageBuilder.SetEntrypointAndCmd(imageEntrypoint, imageCmd);
126128

127-
foreach (KeyValuePair<string, string> label in labels)
129+
if (generateLabels)
128130
{
129-
// labels are validated by System.CommandLine API
130-
imageBuilder.AddLabel(label.Key, label.Value);
131+
foreach (KeyValuePair<string, string> label in labels)
132+
{
133+
// labels are validated by System.CommandLine API
134+
imageBuilder.AddLabel(label.Key, label.Value);
135+
}
136+
137+
if (generateDigestLabel)
138+
{
139+
imageBuilder.AddBaseImageDigestLabel();
140+
}
131141
}
142+
132143
foreach (KeyValuePair<string, string> envVar in envVars)
133144
{
134145
imageBuilder.AddEnvironmentVariable(envVar.Key, envVar.Value);

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

Lines changed: 16 additions & 3 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()
@@ -87,6 +95,11 @@ internal void AddLayer(Layer l)
8795
_baseImageConfig.AddLayer(l);
8896
}
8997

98+
internal void AddBaseImageDigestLabel()
99+
{
100+
AddLabel("org.opencontainers.image.base.digest", _baseImageManifest.GetDigest());
101+
}
102+
90103
/// <summary>
91104
/// Adds a label to a base image.
92105
/// </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() ?? 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/net472/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.g
7777
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.set -> void
7878
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.get -> string!
7979
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.set -> void
80+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.get -> bool
81+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.set -> void
82+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.get -> bool
83+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.set -> void
8084
override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolName.get -> string!
8185
override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateCommandLineCommands() -> string!
8286
override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateFullPathToTool() -> string!

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeI
1313
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.set -> void
1414
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.get -> bool
1515
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.set -> void
16-
static Microsoft.NET.Build.Containers.ContainerBuilder.ContainerizeAsync(System.IO.DirectoryInfo! publishDirectory, string! workingDir, string! baseRegistry, string! baseImageName, string! baseImageTag, string![]! entrypoint, string![]! entrypointArgs, string![]! defaultArgs, string![]! appCommand, string![]! appCommandArgs, string! appCommandInstruction, string! imageName, string![]! imageTags, string? outputRegistry, System.Collections.Generic.Dictionary<string!, string!>! labels, Microsoft.NET.Build.Containers.Port[]? exposedPorts, System.Collections.Generic.Dictionary<string!, string!>! envVars, string! containerRuntimeIdentifier, string! ridGraphPath, string! localRegistry, string? containerUser, string? archiveOutputPath, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<int>!
1716
static readonly Microsoft.NET.Build.Containers.Constants.Version -> string!
18-
Microsoft.NET.Build.Containers.ContainerBuilder
1917
Microsoft.NET.Build.Containers.ContainerHelpers
2018
Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError
2119
Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortNumber = 1 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError
@@ -73,6 +71,8 @@ Microsoft.NET.Build.Containers.ManifestV2
7371
Microsoft.NET.Build.Containers.ManifestV2.Config.get -> Microsoft.NET.Build.Containers.ManifestConfig
7472
Microsoft.NET.Build.Containers.ManifestV2.Config.init -> void
7573
Microsoft.NET.Build.Containers.ManifestV2.GetDigest() -> string!
74+
Microsoft.NET.Build.Containers.ManifestV2.KnownDigest.get -> string?
75+
Microsoft.NET.Build.Containers.ManifestV2.KnownDigest.set -> void
7676
Microsoft.NET.Build.Containers.ManifestV2.Layers.get -> System.Collections.Generic.List<Microsoft.NET.Build.Containers.ManifestLayer>!
7777
Microsoft.NET.Build.Containers.ManifestV2.Layers.init -> void
7878
Microsoft.NET.Build.Containers.ManifestV2.ManifestV2() -> void
@@ -192,6 +192,10 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolPath.get -> string!
192192
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolPath.set -> void
193193
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.get -> string!
194194
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.set -> void
195+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.get -> bool
196+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.set -> void
197+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.get -> bool
198+
Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.set -> void
195199
Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties
196200
Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerEnvironmentVariables.get -> Microsoft.Build.Framework.ITaskItem![]!
197201
Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerEnvironmentVariables.set -> void
@@ -252,12 +256,6 @@ Microsoft.NET.Build.Containers.ManifestLayer.Equals(Microsoft.NET.Build.Containe
252256
~override Microsoft.NET.Build.Containers.Descriptor.Equals(object obj) -> bool
253257
Microsoft.NET.Build.Containers.ManifestLayer.Deconstruct(out string! mediaType, out long size, out string! digest, out string![]? urls) -> void
254258
Microsoft.NET.Build.Containers.Descriptor.Equals(Microsoft.NET.Build.Containers.Descriptor other) -> bool
255-
~override Microsoft.NET.Build.Containers.ManifestV2.ToString() -> string
256-
static Microsoft.NET.Build.Containers.ManifestV2.operator !=(Microsoft.NET.Build.Containers.ManifestV2 left, Microsoft.NET.Build.Containers.ManifestV2 right) -> bool
257-
static Microsoft.NET.Build.Containers.ManifestV2.operator ==(Microsoft.NET.Build.Containers.ManifestV2 left, Microsoft.NET.Build.Containers.ManifestV2 right) -> bool
258-
override Microsoft.NET.Build.Containers.ManifestV2.GetHashCode() -> int
259-
~override Microsoft.NET.Build.Containers.ManifestV2.Equals(object obj) -> bool
260-
Microsoft.NET.Build.Containers.ManifestV2.Equals(Microsoft.NET.Build.Containers.ManifestV2 other) -> bool
261259
~override Microsoft.NET.Build.Containers.ManifestListV2.ToString() -> string
262260
static Microsoft.NET.Build.Containers.ManifestListV2.operator !=(Microsoft.NET.Build.Containers.ManifestListV2 left, Microsoft.NET.Build.Containers.ManifestListV2 right) -> bool
263261
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: 36 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,15 @@ 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.ReadFromJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false);
274+
if (manifest is null) throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys);
275+
manifest.KnownDigest = matchingManifest.digest;
258276
return await ReadSingleImageAsync(
259277
repositoryName,
260-
await manifestResponse.Content.ReadFromJsonAsync<ManifestV2>(cancellationToken: cancellationToken).ConfigureAwait(false),
278+
manifest,
261279
cancellationToken).ConfigureAwait(false);
262-
} else
280+
}
281+
else
263282
{
264283
throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys);
265284
}
@@ -332,13 +351,13 @@ internal async Task<FinalizeUploadInformation> UploadBlobChunkedAsync(Stream con
332351

333352
int bytesRead = await contents.ReadAsync(chunkBackingStore, cancellationToken).ConfigureAwait(false);
334353

335-
ByteArrayContent content = new (chunkBackingStore, offset: 0, count: bytesRead);
354+
ByteArrayContent content = new(chunkBackingStore, offset: 0, count: bytesRead);
336355
content.Headers.ContentLength = bytesRead;
337356

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

@@ -420,7 +439,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source,
420439
}
421440

422441
// 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))
442+
if (!await _registryAPI.Blob.Upload.TryMountAsync(destination.Repository, source.Repository, digest, cancellationToken).ConfigureAwait(false))
424443
{
425444
// The blob wasn't already available in another namespace, so fall back to explicitly uploading it
426445

@@ -432,7 +451,8 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source,
432451
await destinationRegistry.PushLayerAsync(Layer.FromDescriptor(descriptor), destination.Repository, cancellationToken).ConfigureAwait(false);
433452
_logger.LogInformation(Strings.Registry_LayerUploaded, digest, destinationRegistry.RegistryName);
434453
}
435-
else {
454+
else
455+
{
436456
throw new NotImplementedException(Resource.GetString(nameof(Strings.MissingLinkToRegistry)));
437457
}
438458
}
@@ -444,7 +464,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source,
444464
}
445465
else
446466
{
447-
foreach(var descriptor in builtImage.LayerDescriptors)
467+
foreach (var descriptor in builtImage.LayerDescriptors)
448468
{
449469
await uploadLayerFunc(descriptor).ConfigureAwait(false);
450470
}

src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)