diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs index c9a84b8a95d4..c63227808ca1 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -6,9 +6,9 @@ namespace Microsoft.NET.Build.Containers; -public static class ContainerBuilder +internal static class ContainerBuilder { - public static async Task ContainerizeAsync( + internal static async Task ContainerizeAsync( DirectoryInfo publishDirectory, string workingDir, string baseRegistry, @@ -31,6 +31,8 @@ public static async Task ContainerizeAsync( string localRegistry, string? containerUser, string? archiveOutputPath, + bool generateLabels, + bool generateDigestLabel, ILoggerFactory loggerFactory, CancellationToken cancellationToken) { @@ -124,11 +126,20 @@ public static async Task ContainerizeAsync( } imageBuilder.SetEntrypointAndCmd(imageEntrypoint, imageCmd); - foreach (KeyValuePair label in labels) + if (generateLabels) { - // labels are validated by System.CommandLine API - imageBuilder.AddLabel(label.Key, label.Value); + foreach (KeyValuePair label in labels) + { + // labels are validated by System.CommandLine API + imageBuilder.AddLabel(label.Key, label.Value); + } + + if (generateDigestLabel) + { + imageBuilder.AddBaseImageDigestLabel(); + } } + foreach (KeyValuePair envVar in envVars) { imageBuilder.AddEnvironmentVariable(envVar.Key, envVar.Value); diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs index 7bfdcb809962..92d81fc8813a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs @@ -14,6 +14,10 @@ namespace Microsoft.NET.Build.Containers; /// internal sealed class ImageBuilder { + // a snapshot of the manifest that this builder is based on + private readonly ManifestV2 _baseImageManifest; + + // the mutable internal manifest that we're building by modifying the base and applying customizations private readonly ManifestV2 _manifest; private readonly ImageConfig _baseImageConfig; private readonly ILogger _logger; @@ -33,7 +37,8 @@ internal sealed class ImageBuilder internal ImageBuilder(ManifestV2 manifest, ImageConfig baseImageConfig, ILogger logger) { - _manifest = manifest; + _baseImageManifest = manifest; + _manifest = new ManifestV2() { SchemaVersion = manifest.SchemaVersion, Config = manifest.Config, Layers = new(manifest.Layers), MediaType = manifest.MediaType }; _baseImageConfig = baseImageConfig; _logger = logger; } @@ -63,9 +68,12 @@ internal BuiltImage Build() size = imageSize }; - ManifestV2 newManifest = _manifest with + ManifestV2 newManifest = new ManifestV2() { - Config = newManifestConfig + Config = newManifestConfig, + SchemaVersion = _manifest.SchemaVersion, + MediaType = _manifest.MediaType, + Layers = _manifest.Layers }; return new BuiltImage() @@ -87,6 +95,11 @@ internal void AddLayer(Layer l) _baseImageConfig.AddLayer(l); } + internal void AddBaseImageDigestLabel() + { + AddLabel("org.opencontainers.image.base.digest", _baseImageManifest.GetDigest()); + } + /// /// Adds a label to a base image. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs b/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs index eb00143665c6..30937d782a4b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs @@ -12,8 +12,11 @@ namespace Microsoft.NET.Build.Containers; /// /// https://github.com/opencontainers/image-spec/blob/main/manifest.md /// -public readonly record struct ManifestV2 +public class ManifestV2 { + [JsonIgnore] + public string? KnownDigest { get; set; } + /// /// This REQUIRED property specifies the image manifest schema version. /// 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 /// /// Gets the digest for this manifest. /// - public string GetDigest() => DigestUtils.GetDigest(JsonSerializer.SerializeToNode(this)?.ToJsonString() ?? string.Empty); + public string GetDigest() => KnownDigest ??= DigestUtils.GetDigest(JsonSerializer.SerializeToNode(this)?.ToJsonString() ?? string.Empty); } public record struct ManifestConfig(string mediaType, long size, string digest); -public record struct ManifestLayer(string mediaType, long size, string digest, string[]? urls); +public record struct ManifestLayer(string mediaType, long size, string digest, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)][field: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string[]? urls); diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt index 89caa7583181..dbe4406eb596 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -77,6 +77,10 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.g Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.get -> bool +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.get -> bool +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.set -> void override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolName.get -> string! override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateCommandLineCommands() -> string! override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateFullPathToTool() -> string! diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 3b6d27ca2c1e..f4c70d1e7a8b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -13,9 +13,7 @@ Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeI Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.get -> bool Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.set -> void -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! labels, Microsoft.NET.Build.Containers.Port[]? exposedPorts, System.Collections.Generic.Dictionary! envVars, string! containerRuntimeIdentifier, string! ridGraphPath, string! localRegistry, string? containerUser, string? archiveOutputPath, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static readonly Microsoft.NET.Build.Containers.Constants.Version -> string! -Microsoft.NET.Build.Containers.ContainerBuilder Microsoft.NET.Build.Containers.ContainerHelpers Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortNumber = 1 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError @@ -73,6 +71,8 @@ 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 Microsoft.NET.Build.Containers.ManifestV2.GetDigest() -> string! +Microsoft.NET.Build.Containers.ManifestV2.KnownDigest.get -> string? +Microsoft.NET.Build.Containers.ManifestV2.KnownDigest.set -> void Microsoft.NET.Build.Containers.ManifestV2.Layers.get -> System.Collections.Generic.List! Microsoft.NET.Build.Containers.ManifestV2.Layers.init -> void Microsoft.NET.Build.Containers.ManifestV2.ManifestV2() -> void @@ -192,6 +192,10 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolPath.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolPath.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.get -> bool +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.get -> bool +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.set -> void Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerEnvironmentVariables.get -> Microsoft.Build.Framework.ITaskItem![]! Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerEnvironmentVariables.set -> void @@ -252,12 +256,6 @@ Microsoft.NET.Build.Containers.ManifestLayer.Equals(Microsoft.NET.Build.Containe ~override Microsoft.NET.Build.Containers.Descriptor.Equals(object obj) -> bool Microsoft.NET.Build.Containers.ManifestLayer.Deconstruct(out string! mediaType, out long size, out string! digest, out string![]? urls) -> void Microsoft.NET.Build.Containers.Descriptor.Equals(Microsoft.NET.Build.Containers.Descriptor other) -> bool -~override Microsoft.NET.Build.Containers.ManifestV2.ToString() -> string -static Microsoft.NET.Build.Containers.ManifestV2.operator !=(Microsoft.NET.Build.Containers.ManifestV2 left, Microsoft.NET.Build.Containers.ManifestV2 right) -> bool -static Microsoft.NET.Build.Containers.ManifestV2.operator ==(Microsoft.NET.Build.Containers.ManifestV2 left, Microsoft.NET.Build.Containers.ManifestV2 right) -> bool -override Microsoft.NET.Build.Containers.ManifestV2.GetHashCode() -> int -~override Microsoft.NET.Build.Containers.ManifestV2.Equals(object obj) -> bool -Microsoft.NET.Build.Containers.ManifestV2.Equals(Microsoft.NET.Build.Containers.ManifestV2 other) -> bool ~override Microsoft.NET.Build.Containers.ManifestListV2.ToString() -> string static Microsoft.NET.Build.Containers.ManifestListV2.operator !=(Microsoft.NET.Build.Containers.ManifestListV2 left, Microsoft.NET.Build.Containers.ManifestListV2 right) -> bool static Microsoft.NET.Build.Containers.ManifestListV2.operator ==(Microsoft.NET.Build.Containers.ManifestListV2 left, Microsoft.NET.Build.Containers.ManifestListV2 right) -> bool diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs index 68456fa71bfa..21ba5d8c9271 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs @@ -6,12 +6,14 @@ using NuGet.RuntimeModel; using System.Diagnostics; using System.Net.Http.Json; +using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; namespace Microsoft.NET.Build.Containers; -internal interface IManifestPicker { +internal interface IManifestPicker +{ public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary manifestList, string runtimeIdentifier); } @@ -26,7 +28,8 @@ public RidGraphManifestPicker(string runtimeIdentifierGraphPath) public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary ridManifestDict, string runtimeIdentifier) { var bestManifestRid = GetBestMatchingRid(_runtimeGraph, runtimeIdentifier, ridManifestDict.Keys); - if (bestManifestRid is null) { + if (bestManifestRid is null) + { return null; } return ridManifestDict[bestManifestRid]; @@ -132,7 +135,8 @@ private static string DeriveRegistryName(Uri baseUri) /// /// Google Artifact Registry locations (one for each availability zone) are of the form "ZONE-docker.pkg.dev". /// - public bool IsGoogleArtifactRegistry { + public bool IsGoogleArtifactRegistry + { get => RegistryName.EndsWith("-docker.pkg.dev", StringComparison.Ordinal); } @@ -151,7 +155,7 @@ public async Task GetImageManifestAsync(string repositoryName, str { SchemaTypes.DockerManifestV2 or SchemaTypes.OciManifestV1 => await ReadSingleImageAsync( repositoryName, - await initialManifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false), + await ReadManifest().ConfigureAwait(false), cancellationToken).ConfigureAwait(false), SchemaTypes.DockerManifestListV2 => await PickBestImageFromManifestListAsync( repositoryName, @@ -167,6 +171,17 @@ await initialManifestResponse.Content.ReadFromJsonAsync(cancella BaseUri, unknownMediaType)) }; + + async Task ReadManifest() + { + initialManifestResponse.Headers.TryGetValues("Docker-Content-Digest", out var knownDigest); + var manifest = (await initialManifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false))!; + if (knownDigest?.FirstOrDefault() is string knownDigestValue) + { + manifest.KnownDigest = knownDigestValue; + } + return manifest; + } } internal async Task GetManifestListAsync(string repositoryName, string reference, CancellationToken cancellationToken) @@ -193,11 +208,12 @@ private async Task ReadSingleImageAsync(string repositoryName, Man return new ImageBuilder(manifest, new ImageConfig(configDoc), _logger); } - + private static IReadOnlyDictionary GetManifestsByRid(ManifestListV2 manifestList) { var ridDict = new Dictionary(); - foreach (var manifest in manifestList.manifests) { + foreach (var manifest in manifestList.manifests) + { if (CreateRidForPlatform(manifest.platform) is { } rid) { ridDict.TryAdd(rid, manifest); @@ -206,7 +222,7 @@ private static IReadOnlyDictionary GetManifest return ridDict; } - + private static string? CreateRidForPlatform(PlatformInformation platform) { // we only support linux and windows containers explicitly, so anything else we should skip past. @@ -220,7 +236,7 @@ private static IReadOnlyDictionary GetManifest // TODO: we _may_ need OS-specific version parsing. Need to do more research on what the field looks like across more manifest lists. var versionPart = platform.version?.Split('.') switch { - [var major, .. ] => major, + [var major, ..] => major, _ => null }; var platformPart = platform.architecture switch @@ -254,12 +270,15 @@ private async Task PickBestImageFromManifestListAsync( using HttpResponseMessage manifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, matchingManifest.digest, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - + var manifest = await manifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + if (manifest is null) throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys); + manifest.KnownDigest = matchingManifest.digest; return await ReadSingleImageAsync( repositoryName, - await manifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false), + manifest, cancellationToken).ConfigureAwait(false); - } else + } + else { throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys); } @@ -332,13 +351,13 @@ internal async Task UploadBlobChunkedAsync(Stream con int bytesRead = await contents.ReadAsync(chunkBackingStore, cancellationToken).ConfigureAwait(false); - ByteArrayContent content = new (chunkBackingStore, offset: 0, count: bytesRead); + ByteArrayContent content = new(chunkBackingStore, offset: 0, count: bytesRead); content.Headers.ContentLength = bytesRead; // manual because ACR throws an error with the .NET type {"Range":"bytes 0-84521/*","Reason":"the Content-Range header format is invalid"} // content.Headers.Add("Content-Range", $"0-{contents.Length - 1}"); Debug.Assert(content.Headers.TryAddWithoutValidation("Content-Range", $"{chunkStart}-{chunkStart + bytesRead - 1}")); - + NextChunkUploadInformation nextChunk = await _registryAPI.Blob.Upload.UploadChunkAsync(patchUri, content, cancellationToken).ConfigureAwait(false); patchUri = nextChunk.UploadUri; @@ -420,7 +439,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, } // Blob wasn't there; can we tell the server to get it from the base image? - if (! await _registryAPI.Blob.Upload.TryMountAsync(destination.Repository, source.Repository, digest, cancellationToken).ConfigureAwait(false)) + if (!await _registryAPI.Blob.Upload.TryMountAsync(destination.Repository, source.Repository, digest, cancellationToken).ConfigureAwait(false)) { // The blob wasn't already available in another namespace, so fall back to explicitly uploading it @@ -432,7 +451,8 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, await destinationRegistry.PushLayerAsync(Layer.FromDescriptor(descriptor), destination.Repository, cancellationToken).ConfigureAwait(false); _logger.LogInformation(Strings.Registry_LayerUploaded, digest, destinationRegistry.RegistryName); } - else { + else + { throw new NotImplementedException(Resource.GetString(nameof(Strings.MissingLinkToRegistry))); } } @@ -444,7 +464,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, } else { - foreach(var descriptor in builtImage.LayerDescriptors) + foreach (var descriptor in builtImage.LayerDescriptors) { await uploadLayerFunc(descriptor).ConfigureAwait(false); } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs index 10966e412937..7d9ee2b0048d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs @@ -893,5 +893,16 @@ internal static string UnrecognizedMediaType return ResourceManager.GetString("UnrecognizedMediaType", resourceCulture); } } + + /// + /// Looks up a localized string similar to CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created.. + /// + internal static string GenerateDigestLabelWithoutGenerateLabels + { + get + { + return ResourceManager.GetString("GenerateDigestLabelWithoutGenerateLabels", resourceCulture); + } + } } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx index 52f220d00d05..97764561ee6b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx @@ -1,17 +1,17 @@  - - + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + @@ -183,6 +183,7 @@ No host object detected. + CONTAINER1009: Failed to load image from local registry. stdout: {0} @@ -250,6 +251,7 @@ '{0}' was not a valid container image name, it was normalized to '{1}' + CONTAINER2011: {0} '{1}' does not exist @@ -333,12 +335,15 @@ Pushed image '{0}' to {1}. + Pushed image '{0}' to registry '{1}'. + Building image '{0}' with tags '{1}' on top of base image '{2}'. + Error while reading daemon config: {0} @@ -350,12 +355,15 @@ Uploaded config to registry. + Uploading config to registry at blob '{0}', + Layer '{0}' already exists. + Finished uploading layer '{0}' to '{1}'. @@ -409,4 +417,8 @@ local registry via '{0}' {0} is the command used + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf index f9f553aaf43a..055a9be19bd8 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf @@ -142,6 +142,11 @@ CONTAINER1008: Načtení přihlašovacích údajů pro „{0}“ se nezdařilo: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Nebyl zjištěn žádný objekt hostitele. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf index 6e44d62a3cd2..fbed5b0d3e1c 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf @@ -142,6 +142,11 @@ CONTAINER1008: Fehler beim Abrufen der Anmeldeinformationen für „{0}“: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Es wurde kein Hostobjekt erkannt. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf index 0f6db91cd0c5..7364cbdbfe7b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf @@ -142,6 +142,11 @@ CONTAINER1008: No se pudieron recuperar las credenciales de "{0}": {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. No se detectó ningún objeto host. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf index e956008b1359..8afbdbf783fc 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf @@ -142,6 +142,11 @@ CONTAINER1008: échec de la récupération des informations d’identification pour «{0}» : {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Aucun objet hôte détecté. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf index 0aea78393d0c..a730bd5e1543 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf @@ -142,6 +142,11 @@ CONTAINER1008: non è stato possibile recuperare le credenziali per "{0}": {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Nessun oggetto host rilevato. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf index f6bdc23de4a9..34b1388a710d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf @@ -142,6 +142,11 @@ CONTAINER1008: "{0}" の資格情報を取得できませんでした: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. ホスト オブジェクトが検出されませんでした。 diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf index 71a564a7cc8d..c91c2674d3fe 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf @@ -142,6 +142,11 @@ CONTAINER1008: "{0}"에 대한 자격 증명 검색 실패: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. 호스트 개체가 검색되지 않았습니다. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf index 7131b49d1249..88ed0a462dcd 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf @@ -142,6 +142,11 @@ CONTAINER1008: nie można pobrać poświadczeń dla „{0}”: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Nie wykryto obiektu hosta. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf index c79987338301..c42f53aa4e90 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf @@ -142,6 +142,11 @@ CONTAINER1008: Falha ao recuperar credenciais para "{0}": {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Nenhum objeto de host detectado. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf index b33f10b9cc61..8c7e84485486 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf @@ -142,6 +142,11 @@ CONTAINER1008: Не удалось получить учетные данные для "{0}": {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Объект узла не обнаружен. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf index aaa79eda3d82..961fcff03a61 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf @@ -142,6 +142,11 @@ CONTAINER1008: "{0}" için kimlik bilgileri alınamadı: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. Ana bilgisayar nesnesi algılanmadı. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf index 06dfd0303418..fadd6de7ab1c 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf @@ -142,6 +142,11 @@ CONTAINER1008: 检索“{0}”的凭据失败: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. 未检测到主机对象。 diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf index 6718c93391b2..901f86ed7c37 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf @@ -142,6 +142,11 @@ CONTAINER1008: 無法擷取 "{0}" 的認證: {1} {StrBegin="CONTAINER1008: "} + + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. + {StrBegin="CONTAINER2030: "} + No host object detected. 未偵測到主機物件。 diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs index 51944549cba4..19cf905128a3 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -146,6 +146,21 @@ partial class CreateNewImage /// public string ContainerUser { get; set; } + /// + /// If true, the tooling may create labels on the generated images. + /// + [Required] + public bool GenerateLabels { get; set; } + + /// + /// If true, the tooling will generate an org.opencontainers.image.base.digest label on the generated images containing the digest of the chosen base image. + /// + /// + /// Normally this would have been handled in the container targets, but we do not currently _fetch_ the digest of the base image in pure MSBuild, so we do it during generation-time. + /// + [Required] + public bool GenerateDigestLabel { get; set; } + [Output] public string GeneratedContainerManifest { get; set; } @@ -191,6 +206,9 @@ public CreateNewImage() GeneratedContainerDigest = ""; GeneratedArchiveOutputPath = ""; + GenerateLabels = false; + GenerateDigestLabel = false; + TaskResources = Resource.Manager; } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index 3779a49f1112..ced4b0646988 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -120,9 +120,24 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) (string[] entrypoint, string[] cmd) = DetermineEntrypointAndCmd(baseImageEntrypoint: imageBuilder.BaseImageConfig.GetEntrypoint()); imageBuilder.SetEntrypointAndCmd(entrypoint, cmd); - foreach (ITaskItem label in Labels) + if (GenerateLabels) { - imageBuilder.AddLabel(label.ItemSpec, label.GetMetadata("Value")); + foreach (ITaskItem label in Labels) + { + imageBuilder.AddLabel(label.ItemSpec, label.GetMetadata("Value")); + } + + if (GenerateDigestLabel) + { + imageBuilder.AddBaseImageDigestLabel(); + } + } + else + { + if (GenerateDigestLabel) + { + Log.LogMessageFromResources(nameof(Strings.GenerateDigestLabelWithoutGenerateLabels)); + } } SetEnvironmentVariables(imageBuilder, ContainerEnvironmentVariables); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs index 6189d3d74d4e..51869ccb6c54 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs @@ -120,11 +120,11 @@ internal string GenerateCommandLineCommandsInt() builder.AppendSwitchIfNotNull("--appcommandinstruction ", AppCommandInstruction); } - AppendSwitchIfNotNullSantized(builder, "--entrypoint ", nameof(Entrypoint), Entrypoint); - AppendSwitchIfNotNullSantized(builder, "--entrypointargs ", nameof(EntrypointArgs), EntrypointArgs); - AppendSwitchIfNotNullSantized(builder, "--defaultargs ", nameof(DefaultArgs), DefaultArgs); - AppendSwitchIfNotNullSantized(builder, "--appcommand ", nameof(AppCommand), AppCommand); - AppendSwitchIfNotNullSantized(builder, "--appcommandargs ", nameof(AppCommandArgs), AppCommandArgs); + AppendSwitchIfNotNullSanitized(builder, "--entrypoint ", nameof(Entrypoint), Entrypoint); + AppendSwitchIfNotNullSanitized(builder, "--entrypointargs ", nameof(EntrypointArgs), EntrypointArgs); + AppendSwitchIfNotNullSanitized(builder, "--defaultargs ", nameof(DefaultArgs), DefaultArgs); + AppendSwitchIfNotNullSanitized(builder, "--appcommand ", nameof(AppCommand), AppCommand); + AppendSwitchIfNotNullSanitized(builder, "--appcommandargs ", nameof(AppCommandArgs), AppCommandArgs); if (Labels.Any(e => string.IsNullOrWhiteSpace(e.ItemSpec))) { @@ -192,9 +192,19 @@ internal string GenerateCommandLineCommandsInt() builder.AppendSwitchIfNotNull("--archiveoutputpath ", ArchiveOutputPath); } + if (GenerateLabels) + { + builder.AppendSwitch("--generate-labels"); + } + + if (GenerateDigestLabel) + { + builder.AppendSwitch("--generate-digest-label"); + } + return builder.ToString(); - void AppendSwitchIfNotNullSantized(CommandLineBuilder builder, string commandArgName, string propertyName, ITaskItem[] value) + void AppendSwitchIfNotNullSanitized(CommandLineBuilder builder, string commandArgName, string propertyName, ITaskItem[] value) { ITaskItem[] santized = value.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec)).ToArray(); if (santized.Length != value.Length) diff --git a/src/Containers/containerize/ContainerizeCommand.cs b/src/Containers/containerize/ContainerizeCommand.cs index 7a64e403a0e7..52b7e390b503 100644 --- a/src/Containers/containerize/ContainerizeCommand.cs +++ b/src/Containers/containerize/ContainerizeCommand.cs @@ -23,7 +23,7 @@ internal class ContainerizeCommand : CliRootCommand Required = true }; - internal CliOption BaseImageNameOption { get; } = new("--baseimagename") + internal CliOption BaseImageNameOption { get; } = new("--baseimagename") { Description = "The base image to pull.", Required = true @@ -185,6 +185,18 @@ internal class ContainerizeCommand : CliRootCommand internal CliOption ContainerUserOption { get; } = new("--container-user") { Description = "User to run the container as." }; + internal CliOption GenerateLabelsOption { get; } = new("--generate-labels") + { + Description = "If true, the tooling may create labels on the generated images.", + Arity = ArgumentArity.Zero + }; + + internal CliOption GenerateDigestLabelOption { get; } = new("--generate-digest-label") + { + Description = "If true, the tooling will generate an 'org.opencontainers.image.base.digest' label on the generated images containing the digest of the chosen base image.", + Arity = ArgumentArity.Zero + }; + internal ContainerizeCommand() : base("Containerize an application without Docker.") { PublishDirectoryArgument.AcceptLegalFilePathsOnly(); @@ -211,6 +223,8 @@ internal ContainerizeCommand() : base("Containerize an application without Docke LocalRegistryOption.AcceptOnlyFromAmong(KnownLocalRegistryTypes.SupportedLocalRegistryTypes); this.Options.Add(LocalRegistryOption); this.Options.Add(ContainerUserOption); + this.Options.Add(GenerateLabelsOption); + this.Options.Add(GenerateDigestLabelOption); this.SetAction(async (parseResult, cancellationToken) => { @@ -236,6 +250,8 @@ internal ContainerizeCommand() : base("Containerize an application without Docke string _ridGraphPath = parseResult.GetValue(RidGraphPathOption)!; string _localContainerDaemon = parseResult.GetValue(LocalRegistryOption)!; string? _containerUser = parseResult.GetValue(ContainerUserOption); + bool _generateLabels = parseResult.GetValue(GenerateLabelsOption); + bool _generateDigestLabel = parseResult.GetValue(GenerateDigestLabelOption); //setup basic logging bool traceEnabled = Env.GetEnvironmentVariableAsBool("CONTAINERIZE_TRACE_LOGGING_ENABLED"); @@ -265,6 +281,8 @@ await ContainerBuilder.ContainerizeAsync( _localContainerDaemon, _containerUser, _archiveOutputPath, + _generateLabels, + _generateDigestLabel, loggerFactory, cancellationToken).ConfigureAwait(false); }); diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets index ecb8dee489c7..15af224f67e0 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets @@ -236,7 +236,9 @@ ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)" ContainerRuntimeIdentifier="$(ContainerRuntimeIdentifier)" ContainerUser="$(ContainerUser)" - RuntimeIdentifierGraphPath="$(RuntimeIdentifierGraphPath)"> + RuntimeIdentifierGraphPath="$(RuntimeIdentifierGraphPath)" + GenerateLabels="$(ContainerGenerateLabels)" + GenerateDigestLabel="$(ContainerGenerateLabelsImageBaseDigest)"> diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs index ba6ff51fdbd4..e09946d14fc2 100644 --- a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs @@ -551,6 +551,34 @@ public void Logging_TraceLoggingIsDisabledByDefault() .And.NotHaveStdOutContaining("Trace logging: enabled."); } + [Fact] + public void GenerateCommandLineCommands_LabelGeneration() + { + CreateNewImage task = new(); + + List warnings = new(); + IBuildEngine buildEngine = A.Fake(); + + task.BuildEngine = buildEngine; + + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + task.GenerateLabels = true; + task.GenerateDigestLabel = true; + + string args = task.GenerateCommandLineCommandsInt(); + + Assert.Contains("--generate-labels", args); + Assert.Contains("--generate-digest-label", args); + } + + + private static string GetPathToContainerize() { return Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "containerize"); diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs index a00f2fd47554..1606e26f5bc6 100644 --- a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs @@ -175,6 +175,34 @@ public void ShouldTrimTrailingGitSuffixFromRepoUrls(string repoUrl, string expec .And.ContainSingle(label => LabelMatch("org.opencontainers.image.source", expectedLabel, label), String.Join(",", logger.AllMessages)); } + [InlineData(true)] + [InlineData(false)] + [Theory] + public void ShouldIncludeBaseImageLabelsUnlessUserOptsOut(bool includeBaseImageLabels) + { + var expectedBaseImage = "mcr.microsoft.com/dotnet/runtime:7.0"; + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["ContainerGenerateLabelsImageBaseName"] = includeBaseImageLabels.ToString(), + ["ContainerBaseImage"] = expectedBaseImage, + ["ContainerGenerateLabels"] = true.ToString() + }, projectName: $"{nameof(ShouldIncludeBaseImageLabelsUnlessUserOptsOut)}_{includeBaseImageLabels}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[] { ComputeContainerConfig }, new[] { logger }, null, out var outputs).Should().BeTrue("Build should have succeeded but failed due to {0}", String.Join("\n", logger.AllMessages)); + var labels = instance.GetItems(ContainerLabel); + if (includeBaseImageLabels) + { + labels.Should().NotBeEmpty("Should have evaluated some labels by default") + .And.ContainSingle(label => LabelMatch("org.opencontainers.image.base.name", expectedBaseImage, label)); + } + else + { + labels.Should().NotBeEmpty("Should have evaluated some labels by default") + .And.NotContain(label => LabelMatch("org.opencontainers.image.base.name", expectedBaseImage, label)); + }; + } + [InlineData("7.0.100", "v7.0", "7.0")] [InlineData("7.0.100-preview.7", "v7.0", "7.0")] [InlineData("7.0.100-rc.1", "v7.0", "7.0")] diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs index 559bd7d33c86..c353c4b28565 100644 --- a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs @@ -13,6 +13,8 @@ public class ImageBuilderTests { private readonly TestLoggerFactory _loggerFactory; + private static readonly string StaticKnownDigestValue = "sha256:338c0b702da88157ba4bb706678e43346ece2e4397b888d59fb2d9f6113c8070"; + public ImageBuilderTests(ITestOutputHelper output) { _loggerFactory = new TestLoggerFactory(output); @@ -566,8 +568,8 @@ public void CanSetPortFromEnvVarFromUser(string envVar, string envValue, params [Fact] public void CanSetContainerUserAndOverrideAppUID() { - var userId = "1646"; - var baseConfigBuilder = FromBaseImageConfig($$""" + var userId = "1646"; + var baseConfigBuilder = FromBaseImageConfig($$""" { "architecture": "amd64", "config": { @@ -597,9 +599,9 @@ public void CanSetContainerUserAndOverrideAppUID() } """); - baseConfigBuilder.SetUser(userId); - var config = JsonNode.Parse(baseConfigBuilder.Build().Config); - config!["config"]?["User"]?.GetValue().Should().Be(expected: userId, because: "The precedence of SetUser should override inferred user ids"); + baseConfigBuilder.SetUser(userId); + var config = JsonNode.Parse(baseConfigBuilder.Build().Config); + config!["config"]?["User"]?.GetValue().Should().Be(expected: userId, because: "The precedence of SetUser should override inferred user ids"); } [Fact] @@ -644,6 +646,47 @@ public void WhenMultipleUrlSourcesAreSetOnlyAspnetcoreUrlsIsUsed() Assert.Equal([12345], assignedPorts); } + [Fact] + public void CanSetBaseImageDigestLabel() + { + var builder = FromBaseImageConfig($$""" + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "WorkingDir": "" + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """); + + builder.AddBaseImageDigestLabel(); + var builtImage = builder.Build(); + JsonNode? result = JsonNode.Parse(builtImage.Config); + Assert.NotNull(result); + var labels = result["config"]?["Labels"]?.AsObject(); + var digest = labels?.AsEnumerable().First(label => label.Key == "org.opencontainers.image.base.digest").Value!; + digest.GetValue().Should().Be(StaticKnownDigestValue); + } + private ImageBuilder FromBaseImageConfig(string baseImageConfig, [CallerMemberName] string testName = "") { var manifest = new ManifestV2() @@ -654,9 +697,10 @@ private ImageBuilder FromBaseImageConfig(string baseImageConfig, [CallerMemberNa { mediaType = "", size = 0, - digest = "sha256:0" + digest = "sha256:" }, - Layers = new List() + Layers = new List(), + KnownDigest = StaticKnownDigestValue }; return new ImageBuilder(manifest, new ImageConfig(baseImageConfig), _loggerFactory.CreateLogger(testName)); }