diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 323600781efe..e03865ac7229 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -376,16 +376,16 @@ https://github.com/dotnet/roslyn-analyzers - 3d61c57c73c3dd5f1f407ef9cd3414d94bf0eaf2 + 5bfaf6aea5cf9d1c924d9adc69916eac3be07880 https://github.com/dotnet/roslyn-analyzers - 3d61c57c73c3dd5f1f407ef9cd3414d94bf0eaf2 + 5bfaf6aea5cf9d1c924d9adc69916eac3be07880 https://github.com/dotnet/roslyn-analyzers - 3d61c57c73c3dd5f1f407ef9cd3414d94bf0eaf2 + 5bfaf6aea5cf9d1c924d9adc69916eac3be07880 diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs index 096f70610377..0df6d3fb633a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs @@ -50,6 +50,9 @@ internal ImageBuilder(ManifestV2 manifest, string manifestMediaType, ImageConfig /// public bool IsWindows => _baseImageConfig.IsWindows; + // For tests + internal string ManifestConfigDigest => _manifest.Config.digest; + /// /// Builds the image configuration ready for further processing. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs new file mode 100644 index 000000000000..bc23073080ea --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.NET.Build.Containers.Resources; +using Microsoft.NET.Build.Containers.Tasks; + +namespace Microsoft.NET.Build.Containers; + +internal readonly struct ImageInfo +{ + internal string Config { get; init; } + internal string ManifestDigest { get; init; } + internal string Manifest { get; init; } + internal string ManifestMediaType { get; init; } + + public override string ToString() => ManifestDigest; +} + +internal static class ImageIndexGenerator +{ + /// + /// Generates an image index from the given images. + /// + /// + /// Returns json string of image index and image index mediaType. + /// + /// + internal static (string, string) GenerateImageIndex(ImageInfo[] imageInfos) + { + if (imageInfos.Length == 0) + { + throw new ArgumentException(string.Format(Strings.ImagesEmpty)); + } + + string manifestMediaType = imageInfos[0].ManifestMediaType; + + if (!imageInfos.All(image => string.Equals(image.ManifestMediaType, manifestMediaType, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(Strings.MixedMediaTypes); + } + + if (manifestMediaType == SchemaTypes.DockerManifestV2) + { + return GenerateImageIndex(imageInfos, SchemaTypes.DockerManifestV2, SchemaTypes.DockerManifestListV2); + } + else if (manifestMediaType == SchemaTypes.OciManifestV1) + { + return GenerateImageIndex(imageInfos, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1); + } + else + { + throw new NotSupportedException(string.Format(Strings.UnsupportedMediaType, manifestMediaType)); + } + } + + private static (string, string) GenerateImageIndex(ImageInfo[] images, string manifestMediaType, string imageIndexMediaType) + { + // Here we are using ManifestListV2 struct, but we could use ImageIndexV1 struct as well. + // We are filling the same fiels, so we can use the same struct. + var manifests = new PlatformSpecificManifest[images.Length]; + for (int i = 0; i < images.Length; i++) + { + var image = images[i]; + + var manifest = new PlatformSpecificManifest + { + mediaType = manifestMediaType, + size = image.Manifest.Length, + digest = image.ManifestDigest, + platform = GetArchitectureAndOsFromConfig(image) + }; + manifests[i] = manifest; + } + + var dockerManifestList = new ManifestListV2 + { + schemaVersion = 2, + mediaType = imageIndexMediaType, + manifests = manifests + }; + + return (JsonSerializer.SerializeToNode(dockerManifestList)?.ToJsonString() ?? "", dockerManifestList.mediaType); + } + + private static PlatformInformation GetArchitectureAndOsFromConfig(ImageInfo image) + { + var configJson = JsonNode.Parse(image.Config) as JsonObject ?? + throw new ArgumentException($"{nameof(image.Config)} should be a JSON object.", nameof(image.Config)); + + var architecture = configJson["architecture"]?.ToString() ?? + throw new ArgumentException($"{nameof(image.Config)} should contain 'architecture'.", nameof(image.Config)); + + var os = configJson["os"]?.ToString() ?? + throw new ArgumentException($"{nameof(image.Config)} should contain 'os'.", nameof(image.Config)); + + return new PlatformInformation { architecture = architecture, os = os }; + } +} 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 6dd6551ddb37..43526e9c3ab4 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 @@ -62,6 +62,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerDigest.get Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerDigest.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedArchiveOutputPath.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedArchiveOutputPath.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerMediaType.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerMediaType.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageTags.get -> string![]! @@ -123,8 +125,8 @@ Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsTrimmed.get Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsTrimmed.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContained.get -> bool Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContained.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.get -> string! -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.set -> void +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifiers.get -> string![]! +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifiers.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.get -> bool Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 1522357b9a11..b19e4fa81718 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -12,12 +12,27 @@ Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsTrimmed.get Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsTrimmed.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContained.get -> bool Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContained.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.get -> string! -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.set -> void +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifiers.get -> string![]! +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifiers.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.get -> string? Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.get -> bool Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.Cancel() -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.CreateImageIndex() -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.Dispose() -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.GeneratedImageIndex.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.GeneratedImageIndex.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.ImageTags.get -> string![]! +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.ImageTags.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.GeneratedContainers.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.GeneratedContainers.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.OutputRegistry.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.OutputRegistry.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.Repository.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.Repository.set -> void +override Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.Execute() -> bool static readonly Microsoft.NET.Build.Containers.Constants.Version -> string! Microsoft.NET.Build.Containers.ContainerHelpers Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError @@ -196,6 +211,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerDigest.get Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerDigest.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedArchiveOutputPath.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedArchiveOutputPath.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerMediaType.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerMediaType.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageTags.get -> string![]! diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs index 14889a669f34..a8b2248d8996 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs @@ -38,10 +38,9 @@ public async Task GetAsync(string repositoryName, string re }; } - public async Task PutAsync(string repositoryName, string reference, ManifestV2 manifest, string mediaType, CancellationToken cancellationToken) + public async Task PutAsync(string repositoryName, string reference, string manifestJson, string mediaType, CancellationToken cancellationToken) { - string jsonString = JsonSerializer.SerializeToNode(manifest)?.ToJsonString() ?? ""; - HttpContent manifestUploadContent = new StringContent(jsonString); + HttpContent manifestUploadContent = new StringContent(manifestJson); manifestUploadContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); HttpResponseMessage putResponse = await _client.PutAsync(new Uri(_baseUri, $"/v2/{repositoryName}/manifests/{reference}"), manifestUploadContent, cancellationToken).ConfigureAwait(false); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs index 16751a5c6685..23381179403d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs @@ -14,5 +14,5 @@ internal interface IManifestOperations { public Task GetAsync(string repositoryName, string reference, CancellationToken cancellationToken); - public Task PutAsync(string repositoryName, string reference, ManifestV2 manifest, string mediaType, CancellationToken cancellationToken); + public Task PutAsync(string repositoryName, string reference, string manifestListJson, string mediaType, CancellationToken cancellationToken); } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs index 6ac75508f459..fe43d88cc822 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using NuGet.Packaging; using System.Diagnostics; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.NET.Build.Containers.Resources; using NuGet.RuntimeModel; @@ -529,6 +529,17 @@ private async Task UploadBlobAsync(string repository, string digest, Stream cont } + public async Task PushManifestListAsync(string repositoryName, string[] tags, string manifestListJson, string mediaType, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var tag in tags) + { + _logger.LogInformation(Strings.Registry_TagUploadStarted, tag, RegistryName); + await _registryAPI.Manifest.PutAsync(repositoryName, tag, manifestListJson, mediaType, cancellationToken).ConfigureAwait(false); + _logger.LogInformation(Strings.Registry_TagUploaded, tag, RegistryName); + } + } + public Task PushAsync(BuiltImage builtImage, SourceImageReference source, DestinationImageReference destination, CancellationToken cancellationToken) => PushAsync(builtImage, source, destination, pushTags: true, cancellationToken); @@ -593,13 +604,14 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, // Tags can refer to an image manifest or an image manifest list. // In the first case, we push tags to the registry. // In the second case, we push the manifest digest so the manifest list can refer to it. + string manifestJson = JsonSerializer.SerializeToNode(builtImage.Manifest)?.ToJsonString() ?? ""; if (pushTags) { Debug.Assert(destination.Tags.Length > 0); foreach (string tag in destination.Tags) { _logger.LogInformation(Strings.Registry_TagUploadStarted, tag, RegistryName); - await _registryAPI.Manifest.PutAsync(destination.Repository, tag, builtImage.Manifest, builtImage.ManifestMediaType, cancellationToken).ConfigureAwait(false); + await _registryAPI.Manifest.PutAsync(destination.Repository, tag, manifestJson, builtImage.ManifestMediaType, cancellationToken).ConfigureAwait(false); _logger.LogInformation(Strings.Registry_TagUploaded, tag, RegistryName); } } @@ -607,7 +619,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, { string manifestDigest = builtImage.Manifest.GetDigest(); _logger.LogInformation(Strings.Registry_ManifestUploadStarted, RegistryName, manifestDigest); - await _registryAPI.Manifest.PutAsync(destination.Repository, manifestDigest, builtImage.Manifest, builtImage.ManifestMediaType, cancellationToken).ConfigureAwait(false); + await _registryAPI.Manifest.PutAsync(destination.Repository, manifestDigest, manifestJson, builtImage.ManifestMediaType, cancellationToken).ConfigureAwait(false); _logger.LogInformation(Strings.Registry_ManifestUploaded, RegistryName); } } 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 0edd6dbaa53e..aff92942f408 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs @@ -168,6 +168,15 @@ internal static string BlobUploadFailed { } } + /// + /// Looks up a localized string similar to Building image index '{0}' on top of manifests {1}.. + /// + internal static string BuildingImageIndex { + get { + return ResourceManager.GetString("BuildingImageIndex", resourceCulture); + } + } + /// /// Looks up a localized string similar to Pushed image '{0}' to {1}.. /// @@ -339,6 +348,15 @@ internal static string HostObjectNotDetected { } } + /// + /// Looks up a localized string similar to Pushed image index '{0}' to registry '{1}'.. + /// + internal static string ImageIndexUploadedToRegistry { + get { + return ResourceManager.GetString("ImageIndexUploadedToRegistry", resourceCulture); + } + } + /// /// Looks up a localized string similar to CONTAINER1009: Failed to load image from local registry. stdout: {0}. /// @@ -357,6 +375,15 @@ internal static string ImagePullNotSupported { } } + /// + /// Looks up a localized string similar to Cannot create manifest list (image index) because no images were provided.. + /// + internal static string ImagesEmpty { + get { + return ResourceManager.GetString("ImagesEmpty", resourceCulture); + } + } + /// /// Looks up a localized string similar to CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring.. /// @@ -366,6 +393,15 @@ internal static string InvalidEnvVar { } } + /// + /// Looks up a localized string similar to Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata.. + /// + internal static string InvalidImageMetadata { + get { + return ResourceManager.GetString("InvalidImageMetadata", resourceCulture); + } + } + /// /// Looks up a localized string similar to CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character.. /// @@ -447,6 +483,15 @@ internal static string InvalidTags { } } + /// + /// Looks up a localized string similar to Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none.. + /// + internal static string InvalidTargetRuntimeIdentifiers { + get { + return ResourceManager.GetString("InvalidTargetRuntimeIdentifiers", resourceCulture); + } + } + /// /// Looks up a localized string similar to CONTAINER1003: Token response had neither token nor access_token.. /// @@ -510,6 +555,15 @@ internal static string MissingPortNumber { } } + /// + /// Looks up a localized string similar to 'mediaType' of manifests should be the same in manifest list (image index).. + /// + internal static string MixedMediaTypes { + get { + return ResourceManager.GetString("MixedMediaTypes", resourceCulture); + } + } + /// /// Looks up a localized string similar to CONTAINER1004: No RequestUri specified.. /// @@ -753,6 +807,15 @@ internal static string UnrecognizedMediaType { } } + /// + /// Looks up a localized string similar to Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'.. + /// + internal static string UnsupportedMediaType { + get { + return ResourceManager.GetString("UnsupportedMediaType", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unable to create tarball for mediaType '{0}'.. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx index f25119f51fd4..6337f4861b8e 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx @@ -201,6 +201,9 @@ CONTAINER2019: Invalid SDK semantic version '{0}'. {StrBegin="CONTAINER2019: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. {StrBegin="CONTAINER2018: "} @@ -347,10 +350,36 @@ Pushed image '{0}' to registry '{1}'. + + Pushed image index '{0}' to registry '{1}'. + + Building image '{0}' with tags '{1}' on top of base image '{2}'. + + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + + + 'mediaType' of manifests should be the same in manifest list (image index). + + + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + + + Cannot create manifest list (image index) because no images were provided. + + + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + Error while reading daemon config: {0} {0} is the exception message that ends with period 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 625eab883915..f0d7173a9cea 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 @@ -57,6 +57,13 @@ CONTAINER1001: Nepovedlo se nahrát objekt blob pomocí {0}; přijatý stavový kód {1} {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. Image {0} byla vložena do {1}. @@ -152,6 +159,11 @@ Nebyl zjištěn žádný objekt hostitele. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Nepodařilo se načíst bitovou kopii z místního registru. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: Načítání imagí z místního registru se nepodporuje. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' není platná proměnná prostředí. Ignorování. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: Odvozený název image „{0}“ obsahuje zcela neplatné znaky. Platné znaky pro název obrázku jsou alfanumerické znaky, -, /, nebo _, a název obrázku musí začínat alfanumerickým znakem. @@ -212,6 +234,11 @@ CONTAINER2010: Byla zadána neplatná {0} : {1}. {0} musí být seznam platných značek obrázků oddělených středníky. Značky obrázků musí být alfanumerické, podtržítka, spojovníky nebo tečky. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: Odpověď tokenu neměla token ani access_token. @@ -247,6 +274,11 @@ CONTAINER2016: Položka ContainerPort '{0}' neurčuje číslo portu. Ujistěte se prosím, že položka Include je číslo portu, například <ContainerPort Include="80" />. {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: Nebyl zadán žádný identifikátor RequestUri. @@ -382,6 +414,11 @@ CONTAINER2001: Nerozpoznaný typ mediaType '{0}'. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Unable to create tarball for mediaType '{0}'. 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 6249a6147f07..ea8fc611e0a0 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 @@ -57,6 +57,13 @@ CONTAINER1001: Fehler beim Hochladen des Blobs mit {0}; der Statuscode „{1}“ wurde empfangen. {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. Bild "{0}" wurde in {1} gepusht. @@ -152,6 +159,11 @@ Es wurde kein Hostobjekt erkannt. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Fehler beim Laden des Images aus der lokalen Registrierung. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: Das Pullen von Images aus der lokalen Registrierung wird nicht unterstützt. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: „{1}“ war keine gültige Umgebungsvariable. Sie wird ignoriert. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: Der abgeleitete Imagename '{0}' enthält vollständig ungültige Zeichen. Die gültigen Zeichen für einen Bildnamen sind alphanumerische Zeichen, -, /, oder _, und der Bildname muss mit einem alphanumerischen Zeichen beginnen. @@ -212,6 +234,11 @@ CONTAINER2010: Ungültige {0} angegeben: {1}. {0} muss eine durch Semikolons getrennte Liste gültiger Imagetags sein. Imagetags müssen alphanumerisch, Unterstrich, Bindestrich oder Punkt sein. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: Die Tokenantwort enthielt weder ein Token noch access_token. @@ -247,6 +274,11 @@ CONTAINER2016: Das ContainerPort-Element „{0}“ gibt keine Portnummer an. Stellen Sie sicher, dass der Include des Elements eine Portnummer ist, z. B. „<ContainerPort Include="80" />“ {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: Es wurde kein RequestUri angegeben. @@ -382,6 +414,11 @@ CONTAINER2001: Unbekannter mediaType „{0}“. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Unable to create tarball for mediaType '{0}'. 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 dcd6b7f40a0e..89f6aa5025c7 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 @@ -57,6 +57,13 @@ CONTAINER1001: no se pudo cargar el blob mediante {0}; se ha recibido el código de estado "{1}". {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. Insertada la imagen “{0}” en {1}. @@ -152,6 +159,11 @@ No se detectó ningún objeto host. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: no se pudo cargar la imagen desde el registro local. Stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: No se admite la extracción de imágenes del registro local. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: "{1}" no era una variable de entorno válida. Ignorando. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: el nombre de imagen inferido '{0}' contiene caracteres totalmente no válidos. Los caracteres válidos para un nombre de imagen son los caracteres alfanuméricos, -, /, o _; el nombre de imagen tiene que comenzar con uno. @@ -212,6 +234,11 @@ CONTAINER2010: se proporcionó un {0} no válido: {1}. {0} debe ser una lista delimitada por punto y coma de etiquetas de imagen válidas. Las etiquetas de imagen deben ser alfanuméricas, con guion bajo, guiones o puntos. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: La respuesta del token no tenía ningún token ni access_token. @@ -247,6 +274,11 @@ CONTAINER2016: El elemento ContainerPort "{0}" no especifica el número de puerto. Asegúrate de que la inclusión del elemento es un número de puerto, por ejemplo, "<ContainerPort Include="80" />" {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: No se especificó RequestUri. @@ -382,6 +414,11 @@ CONTAINER2001: mediaType "{0}" no reconocido. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. No se puede crear un tarball para el mediaType "{0}". 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 180aad48339d..9dcc070cb802 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 @@ -57,6 +57,13 @@ CONTAINER1001: échec du chargement de l’objet blob à l’aide de {0}; le code d’état «{1}» a été reçu. {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. L’image '{0}' a été envoyée à {1}. @@ -152,6 +159,11 @@ Aucun objet hôte détecté. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Échec du chargement de l'image à partir du registre local. sortie standard : {0} @@ -162,11 +174,21 @@ CONTAINER1010: L'extraction d'images à partir du registre local n'est pas prise en charge. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0} : '{1}' n’était pas une variable d’environnement valide. Ignorant. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: le nom d'image déduit '{0}' contient des caractères entièrement non valides. Les caractères valides pour un nom d'image sont les caractères alphanumériques, -, / ou _, et le nom de l'image doit commencer par un caractère alphanumérique. @@ -212,6 +234,11 @@ CONTAINER2010: {0} non valide fournie : {1}. {0} doit être une liste de balises d’image valides délimitées par des points-virgules. Les balises d’image doivent être alphanumériques, traits de soulignement, traits d’union ou point. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: la réponse de jeton n’avait ni jeton ni access_token. @@ -247,6 +274,11 @@ CONTAINER2016: l’élément ContainerPort '{0}' ne spécifie pas le numéro de port. Vérifiez que l’élément Include est un numéro de port, par exemple '<ContainerPort Include="80" />' {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: aucun RequestUri spécifié. @@ -382,6 +414,11 @@ CONTAINER2001: '{0}' mediaType non reconnu. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Impossible de créer tarball pour mediaType '{0}'. 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 3c108c9d90d3..7ab0ca10be53 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 @@ -57,6 +57,13 @@ CONTAINER1001: non è stato possibile caricare il BLOB usando {0}; codice di stato ricevuto '{1}'. {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. È stato eseguito il push dell'immagine '{0}' in {1}. @@ -152,6 +159,11 @@ Nessun oggetto host rilevato. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: non è stato possibile caricare l'immagine dal registro locale. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: il pull di immagini dal registro locale non è supportato. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' non è una variabile di ambiente valida. Il valore verrà ignorato. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: il nome dell'immagine dedotto '{0}' contiene caratteri completamente non validi. I caratteri validi per un nome di immagine sono caratteri alfanumerici, -, / o _, e il nome dell'immagine deve iniziare con un carattere alfanumerico. @@ -212,6 +234,11 @@ CONTAINER2010: il valore {0} specificato non è valido: {1}. {0} deve essere un elenco delimitato da punto e virgola di tag di immagine validi. I tag immagine devono essere alfanumerici, di sottolineatura, trattino o punto. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: la risposta del token non contiene né token né access_token. @@ -247,6 +274,11 @@ CONTAINER2016: l'elemento ContainerPort '{0}' non specifica il numero di porta. Assicurarsi che il valore Include dell'elemento sia un numero di porta, ad esempio '<ContainerPort Include="80" />' {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: nessun RequestUri specificato. @@ -382,6 +414,11 @@ CONTAINER2001: mediaType '{0}' non riconosciuto. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Impossibile creare il tarball per un mediaType '{0}'. 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 88cb18140bcd..9c7ebe912fd4 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 @@ -57,6 +57,13 @@ CONTAINER1001: {0} を使用して BLOB をアップロードできませんでした; 状態コード '{1}' を受信しました。 {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. イメージ '{0}' を {1} にプッシュしました。 @@ -152,6 +159,11 @@ ホスト オブジェクトが検出されませんでした。 + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: ローカル レジストリからイメージを読み込めませんでした。stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: ローカル レジストリからのイメージのプルはサポートされていません。 {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' は有効な環境変数ではありませんでした。無視しています。 {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: 推定されたイメージ名 '{0}' に、完全に無効な文字が含まれています。イメージ名に有効な文字は英数字、-、/、または _で、イメージ名の先頭には英数字を使用する必要があります。 @@ -212,6 +234,11 @@ CONTAINER2010: 無効な {0} が指定されました: {1}。{0} は、セミコロンで区切られた有効なイメージ タグのリストである必要があります。イメージ タグは、英数字、アンダースコア、ハイフン、またはピリオドである必要があります。 {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: トークン応答にトークンも access_token もありませんでした。 @@ -247,6 +274,11 @@ CONTAINER2016: ContainerPort 項目 '{0}' でポート番号が指定されていません。項目の Include がポート番号 (e '<ContainerPort Include="80" />' など) であることを確認してください {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: RequestUri が指定されていません。 @@ -382,6 +414,11 @@ CONTAINER2001: 認識されない mediaType '{0}' です。 {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. mediaType '{0}' の tarball を作成できません。 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 e0738ef570c3..2ff624cec676 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 @@ -57,6 +57,13 @@ CONTAINER1001: {0}을(를) 사용하여 Blob을 업로드하지 못했습니다. '{1}' 상태 코드를 수신했습니다. {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. '{0}' 이미지를 {1}에 푸시했습니다. @@ -152,6 +159,11 @@ 호스트 개체가 검색되지 않았습니다. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: 로컬 레지스트리에서 이미지를 로드하지 못했습니다. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: 로컬 레지스트리에서 이미지 끌어오기가 지원되지 않습니다. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}'은(는) 유효한 환경 변수가 아닙니다. 무시 중. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: 유추된 이미지 이름 '{0}'에 완전히 잘못된 문자가 포함되어 있습니다. 이미지 이름의 유효한 문자는 영숫자, -, /또는 _이며 이미지 이름은 영숫자 문자로 시작해야 합니다. @@ -212,6 +234,11 @@ CONTAINER2010: 잘못된 {0}이(가) 제공됨: {1}. {0}은(는) 세미콜론으로 구분된 유효한 이미지 태그 목록이어야 합니다. 이미지 태그는 영숫자, 밑줄, 하이픈 또는 마침표여야 합니다. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: 토큰 응답에 토큰이나 access_token이 없습니다. @@ -247,6 +274,11 @@ CONTAINER2016: ContainerPort 항목 '{0}'이(가) 포트 번호를 지정하지 않습니다. 항목의 포함이 포트 번호인지 확인하세요(예: '<ContainerPort Include="80" />'). {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: RequestUri가 지정되지 않았습니다. @@ -382,6 +414,11 @@ CONTAINER2001: 미디어 유형 '{0}'을(를) 인식할 수 없습니다. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Unable to create tarball for mediaType '{0}'. 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 988eec6e8358..ee74c48962b2 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 @@ -57,6 +57,13 @@ CONTAINER1001: nie można przekazać obiektu blob przy użyciu {0}; odebrano kod stanu „{1}”. {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. Wypchnięty obraz „{0}” do „{1}”. @@ -152,6 +159,11 @@ Nie wykryto obiektu hosta. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Nie można załadować obrazu z rejestru lokalnego. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: Ściąganie obrazów z rejestru lokalnego nie jest obsługiwane. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: „{1}” nie jest prawidłową zmienną środowiskową. Ignorowanie. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: Wywnioskowana nazwa obrazu „{0}” zawiera całkowicie nieprawidłowe znaki. Prawidłowe znaki nazwy obrazu to znaki alfanumeryczne oraz —, /, lub _, a nazwa obrazu musi zaczynać się znakiem alfanumerycznym. @@ -212,6 +234,11 @@ CONTAINER2010: podano nieprawidłowy {0}: {1}. {0} musi być rozdzielaną średnikami listą prawidłowych tagów obrazów. Tagi obrazów muszą być alfanumeryczne, zawierać podkreślenia, łączniki lub kropki. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: odpowiedź tokenu nie miała tokenu ani access_token. @@ -247,6 +274,11 @@ CONTAINER2016: element ContainerPort „{0}” nie określa numeru portu. Upewnij się, że element Include jest numerem portu, na przykład „<ContainerPort Include="80" />” {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: nie określono identyfikatora RequestUri. @@ -382,6 +414,11 @@ CONTAINER2001: nierozpoznany typ nośnika „{0}”. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Nie można utworzyć elementu tarball dla elementu mediaType „{0}”. 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 194d82ddefb8..cefb88d7f7e6 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 @@ -57,6 +57,13 @@ CONTAINER1001: Falha ao carregar o blob usando {0}; código de status recebido '{1}'. {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. Imagem enviada '{0}' para {1}. @@ -152,6 +159,11 @@ Nenhum objeto de host detectado. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: falha ao carregar a imagem do registro local. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: A extração de imagens do registro local não é suportada. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' não era uma variável de ambiente válida. Ignorando. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: o nome da imagem inferida '{0}' contém caracteres totalmente inválidos. Os caracteres válidos para um nome de imagem são caracteres alfanuméricos, -, / ou _, e o nome da imagem deve começar com um caractere alfanumérico. @@ -212,6 +234,11 @@ CONTAINER2010: inválido {0} fornecido: {1}. {0} deve ser uma lista delimitada por ponto-e-vírgula de marcas de imagem válidas. As tags de imagem devem ser alfanuméricas, sublinhado, hífen ou ponto. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: A resposta do token não tinha token nem access_token. @@ -247,6 +274,11 @@ CONTAINER2016: O item ContainerPort '{0}' não especifica o número da porta. Certifique-se de que o Include do item seja um número de porta, por exemplo '<ContainerPort Include="80" />' {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: Nenhum RequestUri especificado. @@ -382,6 +414,11 @@ CONTAINER2001: MediaType não reconhecido '{0}'. {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Não é possível criar tarball para mediaType "{0}". 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 2c54887d3d89..43563d4a7d84 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 @@ -57,6 +57,13 @@ CONTAINER1001: не удалось отправить BLOB-объект с помощью {0}; получен код состояния "{1}". {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. Изображение "{0}" отправлено в {1}. @@ -152,6 +159,11 @@ Объект узла не обнаружен. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: не удалось загрузить образ из локального реестра. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: извлечение образов из локального реестра не поддерживается. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: "{1}" не является допустимой переменной среды. Пропуск. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: Предполагаемое имя изображения "{0}" содержит совершенно недопустимые символы. Допустимыми символами для имени изображения являются буквенно-цифровые символы, -, / или _, а имя изображения должно начинаться с буквенно-цифрового символа. @@ -212,6 +234,11 @@ CONTAINER2010: предоставлен недопустимый {0}: {1}. {0} должен быть списком допустимых тегов изображений, разделенных точкой с запятой. В качестве тегов изображений допускаются буквы, цифры, символы подчеркивания, дефисы и точки. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: ответ токена не содержит маркера и access_token. @@ -247,6 +274,11 @@ CONTAINER2016: элемент ContainerPort "{0}" не указывает номер порта. Убедитесь, что include элемента является номером порта, например "<ContainerPort Include="80" />" {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: не указан RequestUri. @@ -382,6 +414,11 @@ CONTAINER2001: нераспознанный тип мультимедиа "{0}". {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Не удалось создать tarball для mediaType "{0}". 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 646791f2d72a..e08a51a49a71 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 @@ -57,6 +57,13 @@ CONTAINER1001: Blob, {0} kullanarak karşıya yüklenemedi; '{1}' durum kodu alındı. {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. '{0}' görüntüsü {1} konumuna gönderildi. @@ -152,6 +159,11 @@ Ana bilgisayar nesnesi algılanmadı. + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: Görüntü yerel kayıt defterinden yüklenemedi. stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: Yerel kayıt defterinden görüntü çekme desteklenmiyor. {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' geçerli bir Ortam Değişkeni değildi. Görmezden geliniyor. {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: Çıkarsanan '{0}' görüntü adı tamamen geçersiz karakterler içeriyor. Bir görüntü adında geçerli karakterler şunlardan oluşur: alfasayısal karakterler, -, /, veya _. Görüntü adı alfasayısal karakterle başlamalıdır. @@ -212,6 +234,11 @@ CONTAINER2010: Geçersiz {0} sağlandı: {1}. {0}, geçerli resim etiketlerinin noktalı virgülle ayrılmış listesi olmalıdır. Resim etiketleri alfasayısal, alt çizgi, kısa çizgi veya nokta olmalıdır. {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: Belirteç yanıtında belirteç veya access_token yok. @@ -247,6 +274,11 @@ CONTAINER2016: ContainerPort öğesi ('{0}'), bağlantı noktası numarasını belirtmiyor. Lütfen öğenin Include değerinin bir bağlantı noktası numarası olduğundan emin olun, örneğin '<ContainerPort Include="80" />' {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: RequestUri belirtilmedi. @@ -382,6 +414,11 @@ CONTAINER2001: Tanınmayan mediaType ('{0}'). {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. mediaType '{0}' için tarball oluşturulamıyor. 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 175a9084fc20..8eb6429f7db6 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 @@ -57,6 +57,13 @@ CONTAINER1001: 无法使用 {0} 上传 blob;已收到状态代码“{1}”。 {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. 已将图像“{0}”推送到 {1}。 @@ -152,6 +159,11 @@ 未检测到主机对象。 + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: 未能从本地注册表加载映像。stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: 不支持从本地注册表拉取映像。 {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: "{1}" 不是有效的环境变量。忽略。 {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: 推断的图像名称“{0}”包含完全无效的字符。图像名称的有效字符包括字母数字字符、-、/ 或 _,图像名称必须以字母数字字符开头。 @@ -212,6 +234,11 @@ CONTAINER2010: 提供的 {0} 无效: {1}。{0} 必须是有效图像标记的分号分隔列表。图像标记必须是字母数字、下划线、连字符或句点。 {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: 令牌响应既没有令牌,也没有access_token。 @@ -247,6 +274,11 @@ CONTAINER2016: ContainerPort 项“{0}”未指定端口号。请确保项的 Include 是端口号,例如 "<ContainerPort Include="80" />" {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: 未指定 RequestUri。 @@ -382,6 +414,11 @@ CONTAINER2001: 无法识别 mediaType“{0}”。 {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Unable to create tarball for mediaType '{0}'. 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 0333461575af..892b174bbe97 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 @@ -57,6 +57,13 @@ CONTAINER1001: 無法使用 {0} 上傳 blob; 收到狀態碼 '{1}'。 {StrBegin="CONTAINER1001: "} + + Building image index '{0}' on top of manifests {1}. + Building image index '{0}' on top of manifests {1}. + + {0} is the name of the image index and its tag, {1} is the list of manifests digests + + Pushed image '{0}' to {1}. 已將影像 '{0}' 推送至 {1}。 @@ -152,6 +159,11 @@ 未偵測到主機物件。 + + Pushed image index '{0}' to registry '{1}'. + Pushed image index '{0}' to registry '{1}'. + + CONTAINER1009: Failed to load image from local registry. stdout: {0} CONTAINER1009: 無法從本機登錄載入映像。stdout: {0} @@ -162,11 +174,21 @@ CONTAINER1010: 不支援從本機登錄提取映像。 {StrBegin="CONTAINER1010: "} + + Cannot create manifest list (image index) because no images were provided. + Cannot create manifest list (image index) because no images were provided. + + CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' 不是有效的環境變數。正在忽略。 {StrBegin="CONTAINER2015: "} + + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + Cannot create manifest list (image index) because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. + + CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. CONTAINER2005: 推斷的映像名稱 '{0}' 包含完全無效字元。映像名稱的有效字元是英數字元、-、/ 或 _,並且映像名稱必須以英數字元開頭。 @@ -212,6 +234,11 @@ CONTAINER2010: 提供的 {0} 無效: {1}。{0} 必須是有效映像標記的分號分隔清單。映像標記必須是英數字元、底線、連字號或句號。 {StrBegin="CONTAINER2010: "} + + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + + CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: 權杖回應沒有權杖,也沒有access_token。 @@ -247,6 +274,11 @@ CONTAINER2016: ContainerPort 項目 '{0}' 未指定連接埠號碼。請確保項目的 Include 是連接埠號碼,例如 '<ContainerPort Include="80" />' {StrBegin="CONTAINER2016: "} + + 'mediaType' of manifests should be the same in manifest list (image index). + 'mediaType' of manifests should be the same in manifest list (image index). + + CONTAINER1004: No RequestUri specified. CONTAINER1004: 未指定 RequestUri。 @@ -382,6 +414,11 @@ CONTAINER2001: 無法辨識的 mediaType '{0}'。 {StrBegin="CONTAINER2001: "} + + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + Cannot create manifest list (image index) for the provided 'mediaType' = '{0}'. + + Unable to create tarball for mediaType '{0}'. Unable to create tarball for mediaType '{0}'. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageAndTag.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageAndTag.cs index 836b44e90751..b5573582900b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageAndTag.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageAndTag.cs @@ -44,7 +44,7 @@ public sealed class ComputeDotnetBaseImageAndTag : Microsoft.Build.Utilities.Tas /// If this is set to linux-musl-ARCH then we need to use `alpine` for all containers, and tag on `aot` or `extra` as necessary. /// [Required] - public string TargetRuntimeIdentifier { get; set; } + public string[] TargetRuntimeIdentifiers { get; set; } /// /// If a project is self-contained then it includes a runtime, and so the runtime-deps image should be used. @@ -84,7 +84,7 @@ public sealed class ComputeDotnetBaseImageAndTag : Microsoft.Build.Utilities.Tas FrameworkReferences.Length > 0 && FrameworkReferences.Any(x => x.ItemSpec.Equals("Microsoft.AspNetCore.App", StringComparison.OrdinalIgnoreCase)); - private bool IsMuslRid => TargetRuntimeIdentifier.StartsWith("linux-musl", StringComparison.Ordinal); + private bool IsMuslRid; private bool IsBundledRuntime => IsSelfContained; private bool RequiresInference => String.IsNullOrEmpty(UserBaseImage); @@ -99,7 +99,7 @@ public ComputeDotnetBaseImageAndTag() TargetFrameworkVersion = ""; ContainerFamily = ""; FrameworkReferences = []; - TargetRuntimeIdentifier = ""; + TargetRuntimeIdentifiers = []; UserBaseImage = ""; } @@ -113,16 +113,39 @@ public override bool Execute() } else { - var defaultRegistry = RegistryConstants.MicrosoftContainerRegistryDomain; - if (ComputeRepositoryAndTag(out var repository, out var tag)) + if (TargetRuntimeIdentiriersAreValid()) { - ComputedContainerBaseImage = $"{defaultRegistry}/{repository}:{tag}"; - LogInferencePerformedTelemetry($"{defaultRegistry}/{repository}", tag!); + var defaultRegistry = RegistryConstants.MicrosoftContainerRegistryDomain; + if (ComputeRepositoryAndTag(out var repository, out var tag)) + { + ComputedContainerBaseImage = $"{defaultRegistry}/{repository}:{tag}"; + LogInferencePerformedTelemetry($"{defaultRegistry}/{repository}", tag!); + } } return !Log.HasLoggedErrors; } } + private bool TargetRuntimeIdentiriersAreValid() + { + // For "linux-musl" RIDs we choose the alpine base image. + // And because we compute the base image only once, we need to ensure that all RIDs are "linux-musl" or none of them. + var muslRidsCount = TargetRuntimeIdentifiers.Count(rid => rid.StartsWith("linux-musl", StringComparison.Ordinal)); + if (muslRidsCount > 0) + { + if (muslRidsCount == TargetRuntimeIdentifiers.Length) + { + IsMuslRid = true; + } + else + { + Log.LogError(Resources.Strings.InvalidTargetRuntimeIdentifiers); + return false; + } + } + return true; + } + private string UbuntuCodenameForSDKVersion(SemanticVersion version) { if (version >= SemanticVersion.Parse("8.0.300")) @@ -314,14 +337,14 @@ private void LogNoInferencePerformedTelemetry() containerFamily = ContainerFamily; } } - var telemetryData = new InferenceTelemetryData(InferencePerformed: false, TargetFramework: ParseSemVerToMajorMinor(TargetFrameworkVersion), userBaseImage, userTag, containerFamily, GetTelemetryProjectType(), GetTelemetryPublishMode(), UsesInvariantGlobalization, TargetRuntimeIdentifier); + var telemetryData = new InferenceTelemetryData(InferencePerformed: false, TargetFramework: ParseSemVerToMajorMinor(TargetFrameworkVersion), userBaseImage, userTag, containerFamily, GetTelemetryProjectType(), GetTelemetryPublishMode(), UsesInvariantGlobalization, TargetRuntimeIdentifiers); LogTelemetryData(telemetryData); } private void LogInferencePerformedTelemetry(string imageName, string tag) { // for all inference use cases we will use .NET's images, so we can safely log name, tag, and family - var telemetryData = new InferenceTelemetryData(InferencePerformed: true, TargetFramework: ParseSemVerToMajorMinor(TargetFrameworkVersion), imageName, tag, String.IsNullOrEmpty(ContainerFamily) ? null : ContainerFamily, GetTelemetryProjectType(), GetTelemetryPublishMode(), UsesInvariantGlobalization, TargetRuntimeIdentifier); + var telemetryData = new InferenceTelemetryData(InferencePerformed: true, TargetFramework: ParseSemVerToMajorMinor(TargetFrameworkVersion), imageName, tag, String.IsNullOrEmpty(ContainerFamily) ? null : ContainerFamily, GetTelemetryProjectType(), GetTelemetryPublishMode(), UsesInvariantGlobalization, TargetRuntimeIdentifiers); LogTelemetryData(telemetryData); } @@ -342,7 +365,7 @@ private void LogTelemetryData(InferenceTelemetryData telemetryData) { nameof(telemetryData.ProjectType), telemetryData.ProjectType.ToString() }, { nameof(telemetryData.PublishMode), telemetryData.PublishMode.ToString() }, { nameof(telemetryData.IsInvariant), telemetryData.IsInvariant.ToString() }, - { nameof(telemetryData.TargetRuntime), telemetryData.TargetRuntime } + { nameof(telemetryData.TargetRuntimes), string.Join(";", telemetryData.TargetRuntimes) } }; Log.LogTelemetry("sdk/container/inference", telemetryProperties); } @@ -359,8 +382,8 @@ private void LogTelemetryData(InferenceTelemetryData telemetryData) /// Classifies the project into categories - currently only the broad categories of web/console are known. /// Categorizes the publish mode of the app - FDD, SC, Trimmed, AOT in rough order of complexity/container customization /// We make inference decisions on the invariant-ness of the project, so it's useful to track how often that is used. - /// Different RIDs change the inference calculation, so it's useful to know how different RIDs flow into the results of inference. - private record class InferenceTelemetryData(bool InferencePerformed, string TargetFramework, string? BaseImage, string? BaseImageTag, string? ContainerFamily, ProjectType ProjectType, PublishMode PublishMode, bool IsInvariant, string TargetRuntime); + /// Different RIDs change the inference calculation, so it's useful to know how different RIDs flow into the results of inference. + private record class InferenceTelemetryData(bool InferencePerformed, string TargetFramework, string? BaseImage, string? BaseImageTag, string? ContainerFamily, ProjectType ProjectType, PublishMode PublishMode, bool IsInvariant, string[] TargetRuntimes); private enum ProjectType { AspNetCore, diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs new file mode 100644 index 000000000000..0894686603d2 --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.NET.Build.Containers.Logging; +using Microsoft.NET.Build.Containers.Resources; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.NET.Build.Containers.Tasks; + +public sealed class CreateImageIndex : Microsoft.Build.Utilities.Task, ICancelableTask, IDisposable +{ + #region Parameters + /// + /// Manifests to include in the image index. + /// + [Required] + public ITaskItem[] GeneratedContainers { get; set; } + + /// + /// The registry to push the image index to. + /// + [Required] + public string OutputRegistry { get; set; } + + /// + /// The name of the output image index (manifest list) that will be pushed to the registry. + /// + [Required] + public string Repository { get; set; } + + /// + /// The tag to associate with the new image index (manifest list). + /// + [Required] + public string[] ImageTags { get; set; } + + /// + /// The generated image index (manifest list) in JSON format. + /// + [Output] + public string GeneratedImageIndex { get; set; } + + public CreateImageIndex() + { + GeneratedContainers = Array.Empty(); + OutputRegistry = string.Empty; + Repository = string.Empty; + ImageTags = Array.Empty(); + GeneratedImageIndex = string.Empty; + } + #endregion + + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + public void Cancel() => _cancellationTokenSource.Cancel(); + + public void Dispose() + { + _cancellationTokenSource.Dispose(); + } + + public override bool Execute() + { + try + { + Task.Run(() => ExecuteAsync(_cancellationTokenSource.Token)).GetAwaiter().GetResult(); + } + catch (TaskCanceledException ex) + { + Log.LogWarningFromException(ex); + } + catch (OperationCanceledException ex) + { + Log.LogWarningFromException(ex); + } + return !Log.HasLoggedErrors; + } + + internal async Task ExecuteAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var images = ParseImages(); + if (Log.HasLoggedErrors) + { + return false; + } + + using MSBuildLoggerProvider loggerProvider = new(Log); + ILoggerFactory msbuildLoggerFactory = new LoggerFactory(new[] { loggerProvider }); + ILogger logger = msbuildLoggerFactory.CreateLogger(); + + logger.LogInformation(Strings.BuildingImageIndex, GetRepositoryAndTagsString(), string.Join(", ", images.Select(i => i.ManifestDigest))); + + try + { + (string imageIndex, string mediaType) = ImageIndexGenerator.GenerateImageIndex(images); + + GeneratedImageIndex = imageIndex; + + await PushToRemoteRegistry(GeneratedImageIndex, mediaType, logger, cancellationToken); + } + catch (ContainerHttpException e) + { + if (BuildEngine != null) + { + Log.LogErrorFromException(e, true); + } + } + catch (ArgumentException ex) + { + Log.LogErrorFromException(ex); + } + + return !Log.HasLoggedErrors; + } + + private ImageInfo[] ParseImages() + { + var images = new ImageInfo[GeneratedContainers.Length]; + + for (int i = 0; i < GeneratedContainers.Length; i++) + { + var unparsedImage = GeneratedContainers[i]; + + string config = unparsedImage.GetMetadata("Configuration"); + string manifestDigest = unparsedImage.GetMetadata("ManifestDigest"); + string manifest = unparsedImage.GetMetadata("Manifest"); + string manifestMediaType = unparsedImage.GetMetadata("ManifestMediaType"); + + if (string.IsNullOrEmpty(config) || string.IsNullOrEmpty(manifestDigest) || string.IsNullOrEmpty(manifest)) + { + Log.LogError(Strings.InvalidImageMetadata, unparsedImage.ItemSpec); + break; + } + + images[i] = new ImageInfo + { + Config = config, + ManifestDigest = manifestDigest, + Manifest = manifest, + ManifestMediaType = manifestMediaType + }; + } + + return images; + } + + private async Task PushToRemoteRegistry(string manifestList, string mediaType, ILogger logger, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(ImageTags.Length > 0); + var registry = new Registry(OutputRegistry, logger, RegistryMode.Push); + await registry.PushManifestListAsync(Repository, ImageTags, manifestList, mediaType, cancellationToken).ConfigureAwait(false); + logger.LogInformation(Strings.ImageIndexUploadedToRegistry, GetRepositoryAndTagsString(), OutputRegistry); + } + + private string? _repositoryAndTagsString = null; + + private string GetRepositoryAndTagsString() + { + _repositoryAndTagsString ??= $"{Repository}:{string.Join(", ", ImageTags)}"; + return _repositoryAndTagsString; + } +} 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 4f69658c66d2..3da5135b9b07 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -173,6 +173,9 @@ partial class CreateNewImage [Output] public string GeneratedArchiveOutputPath { get; set; } + [Output] + public string GeneratedContainerMediaType { get; set; } + [Output] public ITaskItem[] GeneratedContainerNames { get; set; } @@ -208,6 +211,7 @@ public CreateNewImage() GeneratedContainerManifest = ""; GeneratedContainerDigest = ""; GeneratedArchiveOutputPath = ""; + GeneratedContainerMediaType = ""; GeneratedContainerNames = Array.Empty(); GenerateLabels = false; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index aa81ae801953..34c4984ae0bb 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -174,6 +174,7 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) GeneratedContainerConfiguration = builtImage.Config; GeneratedContainerDigest = builtImage.Manifest.GetDigest(); GeneratedArchiveOutputPath = ArchiveOutputPath; + GeneratedContainerMediaType = builtImage.ManifestMediaType; GeneratedContainerNames = destinationImageReference.FullyQualifiedImageNames().Select(name => new Microsoft.Build.Utilities.TaskItem(name)).ToArray(); switch (destinationImageReference.Kind) diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props index 4abb098d7c82..80bb59569dd0 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props @@ -15,6 +15,7 @@ + diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets index 11589947cc51..e9e52d5dfece 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets @@ -34,14 +34,21 @@ + $(RuntimeIdentifiers) - $(RuntimeIdentifier) - linux-$(NETCoreSdkPortableRuntimeIdentifier.Split('-')[1]) + $(RuntimeIdentifier) + linux-$(NETCoreSdkPortableRuntimeIdentifier.Split('-')[1]) + <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(ContainerBaseImage)' == ''">true <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(ContainerBaseImage)' != ''">false + + <_TargetRuntimeIdentifiers Include="$(ContainerRuntimeIdentifiers)" Condition="'$(ContainerRuntimeIdentifiers)' != ''" /> + <_TargetRuntimeIdentifiers Include="$(ContainerRuntimeIdentifier)" Condition="'$(ContainerRuntimeIdentifiers)' == ''" /> + + + + + <_TargetRuntimeIdentifiers Remove ="$(_TargetRuntimeIdentifiers)" /> + @@ -99,9 +110,9 @@ - + - + @@ -229,10 +240,8 @@ - - + $(NetCoreRoot) dotnet @@ -271,7 +280,133 @@ + + + + + $(GeneratedContainerManifest) + $(GeneratedContainerConfiguration) + $(GeneratedContainerDigest) + $(GeneratedContainerMediaType) + + + + + + + + + <_rids Include="$(ContainerRuntimeIdentifiers)" Condition="'$(ContainerRuntimeIdentifiers)' != ''" /> + <_rids Include="$(RuntimeIdentifiers)" Condition="'$(ContainerRuntimeIdentifiers)' == '' and '$(RuntimeIdentifiers)' != ''" /> + <_InnerBuild + Include="$(MSBuildProjectFullPath)" + AdditionalProperties=" + ContainerRuntimeIdentifier=%(_rids.Identity); + RuntimeIdentifier=%(_rids.Identity); + ContainerBaseRegistry=$(ContainerBaseRegistry); + ContainerBaseName=$(ContainerBaseName); + ContainerBaseTag=$(ContainerBaseTag); + ContainerRegistry=$(ContainerRegistry); + _ContainerImageTags=@(ContainerImageTags, ';'); + ContainerRepository=$(ContainerRepository); + ContainerWorkingDirectory=$(ContainerWorkingDirectory); + _ContainerEntrypoint=@(ContainerEntrypoint, ';'); + _ContainerEntrypointArgs=@(ContainerEntrypointArgs, ';'); + _ContainerAppCommand=@(ContainerAppCommand, ';'); + _ContainerAppCommandArgs=@(ContainerAppCommandArgs, ';'); + ContainerAppCommandInstruction=$(ContainerAppCommandInstruction); + _ContainerDefaultArgs=@(ContainerDefaultArgs, ';'); + _ContainerLabel=@(ContainerLabel->'%(Identity):%(Value)'); + _ContainerPort=@(ContainerPort->'%(Identity):%(Type)'); + _ContainerEnvironmentVariables=@(ContainerEnvironmentVariable->'%(Identity):%(Value)'); + ContainerUser=$(ContainerUser); + ContainerGenerateLabels=$(ContainerGenerateLabels); + ContainerGenerateLabelsImageBaseDigest=$(ContainerGenerateLabelsImageBaseDigest) + "/> + <_rids Remove ="$(_rids)" /> + + + + + + + + <_SkipCreateImageIndex>false + <_SkipCreateImageIndex Condition="'$(ContainerRegistry)' == ''">true + + + + + + + + + + + + + + + + + + <_ParsedContainerLabel + Condition="'$(_ContainerLabel)' != ':'" + Include="$(_ContainerLabel)"/> + + + <_ParsedContainerPort + Condition="'$(_ContainerPort)' != ':'" + Include="$(_ContainerPort)"/> + + + <_ParsedContainerEnvironmentVariables + Condition="'$(_ContainerEnvironmentVariables)' != ':'" + Include="$(_ContainerEnvironmentVariables)"/> + + + + + $([System.IO.Path]::Combine($(ContainerArchiveOutputPath), $(ContainerRepository)-$(ContainerRuntimeIdentifier).tar.gz)) + + + + + <_IsMultiTFMBuild Condition="'$(TargetFrameworks)' != '' and '$(TargetFramework)' == ''">true + <_IsMultiRIDBuild Condition="'$(BuildingInsideVisualStudio)' != 'true' and (('$(RuntimeIdentifiers)' != '' and '$(RuntimeIdentifier)' == '') or ('$(ContainerRuntimeIdentifiers)' != '' and '$(ContainerRuntimeIdentfier)' == ''))">true + <_IsSingleRIDBuild Condition="'$(_IsMultiRIDBuild)' == ''">true + + + + + + + + + diff --git a/src/SourceBuild/content/eng/Versions.props b/src/SourceBuild/content/eng/Versions.props index 97eea60b3146..4d98dab1f710 100644 --- a/src/SourceBuild/content/eng/Versions.props +++ b/src/SourceBuild/content/eng/Versions.props @@ -23,8 +23,8 @@ of a .NET major or minor release, prebuilts may be needed. When the release is mature, prebuilts are not necessary, and this property is removed from the file. --> - 9.0.100 - 9.0.100-rtm.24529.1 + 9.0.101 + 9.0.101-servicing.24575.1 2.0.0-beta4.24126.1 diff --git a/src/SourceBuild/content/global.json b/src/SourceBuild/content/global.json index 044f23f6237a..dad59de52dde 100644 --- a/src/SourceBuild/content/global.json +++ b/src/SourceBuild/content/global.json @@ -1,6 +1,6 @@ { "tools": { - "dotnet": "9.0.100" + "dotnet": "9.0.101" }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs index 82ccc3d4a2c9..a83e24239f15 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs @@ -7,6 +7,8 @@ static class ContainerCli { public static bool IsPodman => _isPodman.Value; + public static bool IsAvailable => _isAvailable.Value; + public static RunExeCommand PullCommand(ITestOutputHelper log, params string[] args) => CreateCommand(log, "pull", args); @@ -60,4 +62,7 @@ private static RunExeCommand CreateCommand(ITestOutputHelper log, string command private static readonly Lazy _isPodman = new(() => new DockerCli(loggerFactory: new TestLoggerFactory()).GetCommand() == DockerCli.PodmanCommand); + + private static readonly Lazy _isAvailable = + new(() => new DockerCli(loggerFactory: new TestLoggerFactory()).IsAvailable()); } diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateImageIndexTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateImageIndexTests.cs new file mode 100644 index 000000000000..2615b2e178ee --- /dev/null +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateImageIndexTests.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using FakeItEasy; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Build.Containers.IntegrationTests; +using Microsoft.NET.Build.Containers.UnitTests; +using NuGet.Protocol; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.NET.Build.Containers.Tasks.IntegrationTests; + +[Collection("Docker tests")] +public class CreateImageIndexTests +{ + private ITestOutputHelper _testOutput; + + public CreateImageIndexTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + } + + [DockerAvailableFact] + public async Task CreateImageIndex_Baseline() + { + DirectoryInfo newProjectDir = CreateNewProject(); + (IBuildEngine buildEngine, List errors) = SetupBuildEngine(); + string outputRegistry = DockerRegistryManager.LocalRegistry; + string repository = "dotnet/create-image-index-baseline"; + string[] tags = new[] { "tag1", "tag2" }; + + // Create images for 2 rids + TaskItem image1 = PublishAndCreateNewImage("linux-x64", outputRegistry, repository, tags, newProjectDir, buildEngine, errors); + TaskItem image2 = PublishAndCreateNewImage("linux-arm64", outputRegistry, repository, tags, newProjectDir, buildEngine, errors); + + // Create image index + CreateImageIndex cii = new(); + cii.BuildEngine = buildEngine; + cii.OutputRegistry = outputRegistry; + cii.Repository = repository; + cii.ImageTags = tags; + cii.GeneratedContainers = [image1, image2]; + Assert.True(cii.Execute(), FormatBuildMessages(errors)); + + // Assert that the image index is created correctly + cii.GeneratedImageIndex.Should().NotBeNullOrEmpty(); + var imageIndex = cii.GeneratedImageIndex.FromJson(); + imageIndex.manifests.Should().HaveCount(2); + + imageIndex.manifests[0].digest.Should().Be(image1.GetMetadata("ManifestDigest")); + imageIndex.manifests[0].platform.os.Should().Be("linux"); + imageIndex.manifests[0].platform.architecture.Should().Be("amd64"); + + imageIndex.manifests[1].digest.Should().Be(image2.GetMetadata("ManifestDigest")); + imageIndex.manifests[1].platform.os.Should().Be("linux"); + imageIndex.manifests[1].platform.architecture.Should().Be("arm64"); + + // Assert that the image index is pushed to the registry + var loggerFactory = new TestLoggerFactory(_testOutput); + var logger = loggerFactory.CreateLogger(nameof(CreateImageIndex_Baseline)); + Registry registry = new(outputRegistry, logger, RegistryMode.Pull); + + await AssertThatImageIsReferencedInImageIndex("linux-x64", repository, tags, registry); + await AssertThatImageIsReferencedInImageIndex("linux-arm64", repository, tags, registry); + + newProjectDir.Delete(true); + } + + private DirectoryInfo CreateNewProject() + { + DirectoryInfo newProjectDir = new(GetTestDirectoryName()); + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + newProjectDir.Create(); + new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + return newProjectDir; + } + + private TaskItem PublishAndCreateNewImage( + string rid, + string outputRegistry, + string repository, + string[] tags, + DirectoryInfo newProjectDir, + IBuildEngine buildEngine, + List errors) + { + new DotnetCommand(_testOutput, "publish", "-c", "Release", "-r", rid, "--no-self-contained") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + CreateNewImage cni = new(); + + cni.BuildEngine = buildEngine; + + cni.BaseRegistry = "mcr.microsoft.com"; + cni.BaseImageName = "dotnet/runtime"; + cni.BaseImageTag = "7.0"; + + cni.OutputRegistry = outputRegistry; + cni.LocalRegistry = DockerAvailableFactAttribute.LocalRegistry; + cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "Release", ToolsetInfo.CurrentTargetFramework, rid, "publish"); + cni.Repository = repository; + cni.ImageTags = tags.Select(t => $"{t}-{rid}").ToArray(); + cni.WorkingDirectory = "app/"; + cni.ContainerRuntimeIdentifier = rid; + cni.Entrypoint = new TaskItem[] { new("dotnet"), new("build") }; + cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath(); + + Assert.True(cni.Execute(), FormatBuildMessages(errors)); + + TaskItem generatedContainer = new("GeneratedContainer" + rid); + generatedContainer.SetMetadata("Manifest", cni.GeneratedContainerManifest); + generatedContainer.SetMetadata("Configuration", cni.GeneratedContainerConfiguration); + generatedContainer.SetMetadata("ManifestDigest", cni.GeneratedContainerDigest); + generatedContainer.SetMetadata("ManifestMediaType", cni.GeneratedContainerMediaType); + + return generatedContainer; + } + + private async Task AssertThatImageIsReferencedInImageIndex(string rid, string repository, string[] tags, Registry registry) + { + foreach (var tag in tags) + { + var individualImage = await registry.GetImageManifestAsync( + repository, + $"{tag}-{rid}", + rid, + ToolsetUtils.RidGraphManifestPicker, + cancellationToken: default).ConfigureAwait(false); + individualImage.Should().NotBeNull(); + + var imageFromImageIndex = await registry.GetImageManifestAsync( + repository, + tag, + rid, + ToolsetUtils.RidGraphManifestPicker, + cancellationToken: default).ConfigureAwait(false); + imageFromImageIndex.Should().NotBeNull(); + + imageFromImageIndex.ManifestConfigDigest.Should().Be(individualImage.ManifestConfigDigest); + } + } + + private static (IBuildEngine buildEngine, List errors) SetupBuildEngine() + { + List errors = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => errors.Add(e.Message)); + A.CallTo(() => buildEngine.LogErrorEvent(A.Ignored)).Invokes((BuildErrorEventArgs e) => errors.Add(e.Message)); + A.CallTo(() => buildEngine.LogMessageEvent(A.Ignored)).Invokes((BuildMessageEventArgs e) => errors.Add(e.Message)); + + return (buildEngine, errors); + } + + private static string GetTestDirectoryName([CallerMemberName] string testName = "DefaultTest") => Path.Combine(TestSettings.TestArtifactsDirectory, testName + "_" + DateTime.Now.ToString("yyyyMMddHHmmss")); + + private static string FormatBuildMessages(List messages) => string.Join("\r\n", messages); +} + diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs index 66733c52896c..0161ee485886 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs @@ -153,7 +153,7 @@ public void Tasks_EndToEnd_With_EnvironmentVariable_Validation() File.WriteAllText(Path.Combine(newProjectDir.FullName, "Program.cs"), $"Console.Write(Environment.GetEnvironmentVariable(\"GoodEnvVar\"));"); - new DotnetCommand(_testOutput, "build", "--configuration", "release", "/p:runtimeidentifier=linux-x64", $"/p:RuntimeFrameworkVersion={DockerRegistryManager.RuntimeFrameworkVersion}") + new DotnetCommand(_testOutput, "build", "--configuration", "release", "/p:runtimeidentifier=linux-x64") .WithWorkingDirectory(newProjectDir.FullName) .Execute() .Should().Pass(); @@ -162,7 +162,7 @@ public void Tasks_EndToEnd_With_EnvironmentVariable_Validation() (IBuildEngine buildEngine, List errors) = SetupBuildEngine(); pcp.BuildEngine = buildEngine; - pcp.FullyQualifiedBaseImageName = $"mcr.microsoft.com/{DockerRegistryManager.RuntimeBaseImage}:{DockerRegistryManager.Net9PreviewImageTag}"; + pcp.FullyQualifiedBaseImageName = $"mcr.microsoft.com/{DockerRegistryManager.RuntimeBaseImage}:{DockerRegistryManager.Net9ImageTag}"; pcp.ContainerRegistry = ""; pcp.ContainerRepository = "dotnet/envvarvalidation"; pcp.ContainerImageTag = "latest"; @@ -175,7 +175,7 @@ public void Tasks_EndToEnd_With_EnvironmentVariable_Validation() Assert.True(pcp.Execute(), FormatBuildMessages(errors)); Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry); Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage); - Assert.Equal(DockerRegistryManager.Net9PreviewImageTag, pcp.ParsedContainerTag); + Assert.Equal(DockerRegistryManager.Net9ImageTag, pcp.ParsedContainerTag); Assert.Single(pcp.NewContainerEnvironmentVariables); Assert.Equal("Foo", pcp.NewContainerEnvironmentVariables[0].GetMetadata("Value")); diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs index 5734483b579e..7c460b5aad66 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs @@ -15,12 +15,11 @@ public class DockerRegistryManager public const string Net6ImageTag = "6.0"; public const string Net7ImageTag = "7.0"; public const string Net8ImageTag = "8.0"; - public const string Net9PreviewImageTag = "9.0-preview"; - public const string RuntimeFrameworkVersion = "9.0.0-preview.3.24172.9"; + public const string Net9ImageTag = "9.0"; public const string Net8PreviewWindowsSpecificImageTag = $"{Net8ImageTag}-nanoserver-ltsc2022"; public const string LocalRegistry = "localhost:5010"; - public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}/{RuntimeBaseImage}:{Net9PreviewImageTag}"; - public const string FullyQualifiedBaseImageAspNet = $"{BaseImageSource}/{AspNetBaseImage}:{Net9PreviewImageTag}"; + public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}/{RuntimeBaseImage}:{Net9ImageTag}"; + public const string FullyQualifiedBaseImageAspNet = $"{BaseImageSource}/{AspNetBaseImage}:{Net9ImageTag}"; private static string? s_registryContainerId; internal class SameArchManifestPicker : IManifestPicker @@ -72,7 +71,7 @@ public static async Task StartAndPopulateDockerRegistry(ITestOutputHelper testOu EnsureRegistryLoaded(new Uri($"http://{LocalRegistry}"), s_registryContainerId, logger, testOutput); - foreach (string? tag in new[] { Net6ImageTag, Net7ImageTag, Net8ImageTag, Net9PreviewImageTag }) + foreach (string? tag in new[] { Net6ImageTag, Net7ImageTag, Net8ImageTag, Net9ImageTag }) { logger.LogInformation("Pulling image '{repo}/{image}:{tag}'.", BaseImageSource, RuntimeBaseImage, tag); string dotnetdll = System.Reflection.Assembly.GetExecutingAssembly().Location; diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchFact.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchFact.cs new file mode 100644 index 000000000000..36ad9fbc0c9a --- /dev/null +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchFact.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public class DockerIsAvailableAndSupportsArchFactAttribute : FactAttribute +{ + public DockerIsAvailableAndSupportsArchFactAttribute(string arch) + { + if (!DockerSupportsArchHelper.DaemonIsAvailable) + { + base.Skip = "Skipping test because Docker is not available on this host."; + } + else if (!DockerSupportsArchHelper.DaemonSupportsArch(arch)) + { + base.Skip = $"Skipping test because Docker daemon does not support {arch}."; + } + } +} diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs index bf5b8d4bd65d..caafbaea4b45 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs @@ -9,12 +9,6 @@ namespace Microsoft.NET.Build.Containers.IntegrationTests; public class DockerSupportsArchInlineData : DataAttribute { - // an optimization - this doesn't change over time so we can compute it once - private static string[] LinuxPlatforms = GetSupportedLinuxPlatforms(); - - // another optimization - daemons don't switch types easily or quickly, so this is as good as static - private static bool IsWindowsDockerDaemon = GetIsWindowsDockerDaemon(); - private readonly string _arch; private readonly object[] _data; @@ -26,26 +20,40 @@ public DockerSupportsArchInlineData(string arch, params object[] data) public override IEnumerable GetData(MethodInfo testMethod) { - if (DaemonSupportsArch(_arch)) + if (DockerSupportsArchHelper.DaemonSupportsArch(_arch)) { return new object[][] { _data.Prepend(_arch).ToArray() }; - }; + } + else + { + base.Skip = $"Skipping test because Docker daemon does not support {_arch}."; + } return Array.Empty(); } +} + +internal static class DockerSupportsArchHelper +{ + internal static bool DaemonIsAvailable => ContainerCli.IsAvailable; - private bool DaemonSupportsArch(string arch) + internal static bool DaemonSupportsArch(string arch) { + // an optimization - this doesn't change over time so we can compute it once + string[] LinuxPlatforms = GetSupportedLinuxPlatforms(); + if (LinuxPlatforms.Contains(arch)) { return true; } else { + // another optimization - daemons don't switch types easily or quickly, so this is as good as static + bool IsWindowsDockerDaemon = GetIsWindowsDockerDaemon(); + if (IsWindowsDockerDaemon && arch.StartsWith("windows", StringComparison.OrdinalIgnoreCase)) { return true; } - base.Skip = $"Skipping test because Docker daemon does not support {arch}."; return false; } } diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs index 2510235c0f4f..1ea047f9f2d8 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -3,6 +3,8 @@ using System.Formats.Tar; using System.Runtime.CompilerServices; +using System.Text.Json; +using Microsoft.Build.Logging; using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Build.Containers.LocalDaemons; using Microsoft.NET.Build.Containers.Resources; @@ -51,7 +53,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull() ImageBuilder imageBuilder = await registry.GetImageManifestAsync( DockerRegistryManager.RuntimeBaseImage, - DockerRegistryManager.Net9PreviewImageTag, + DockerRegistryManager.Net9ImageTag, "linux-x64", ToolsetUtils.RidGraphManifestPicker, cancellationToken: default).ConfigureAwait(false); @@ -67,7 +69,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull() BuiltImage builtImage = imageBuilder.Build(); // Push the image back to the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" }); await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false); @@ -98,7 +100,7 @@ public async Task ApiEndToEndWithLocalLoad() ImageBuilder imageBuilder = await registry.GetImageManifestAsync( DockerRegistryManager.RuntimeBaseImage, - DockerRegistryManager.Net9PreviewImageTag, + DockerRegistryManager.Net9ImageTag, "linux-x64", ToolsetUtils.RidGraphManifestPicker, cancellationToken: default).ConfigureAwait(false); @@ -113,7 +115,7 @@ public async Task ApiEndToEndWithLocalLoad() BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" }); await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); @@ -139,7 +141,7 @@ public async Task ApiEndToEndWithArchiveWritingAndLoad() ImageBuilder imageBuilder = await registry.GetImageManifestAsync( DockerRegistryManager.RuntimeBaseImage, - DockerRegistryManager.Net9PreviewImageTag, + DockerRegistryManager.Net9ImageTag, "linux-x64", ToolsetUtils.RidGraphManifestPicker, cancellationToken: default).ConfigureAwait(false); @@ -156,7 +158,7 @@ public async Task ApiEndToEndWithArchiveWritingAndLoad() // Write the image to disk var archiveFile = Path.Combine(TestSettings.TestArtifactsDirectory, nameof(ApiEndToEndWithArchiveWritingAndLoad), "app.tar.gz"); - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archiveFile), NewImageName(), new[] { "latest", "1.0" }); await destinationReference.LocalRegistry!.LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); @@ -340,11 +342,6 @@ private string BuildLocalApp([CallerMemberName] string testName = "TestName", st new DotnetCommand(_testOutput, "publish", "-bl", "MinimalTestApp", "-r", rid, "-f", tfm, "-c", "Debug") .WithWorkingDirectory(workingDirectory); - if (tfm == ToolsetInfo.CurrentTargetFramework) - { - publishCommand.Arguments.AddRange(new[] { "-p", $"RuntimeFrameworkVersion={DockerRegistryManager.RuntimeFrameworkVersion}" }); - } - publishCommand.Execute() .Should().Pass(); @@ -508,8 +505,7 @@ public async Task EndToEnd_NoAPI_ProjectType(string projectType, bool addPackage $"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}", $"/p:ContainerRepository={imageName}", $"/p:ContainerImageTag={imageTag}", - "/p:UseRazorSourceGenerator=false", - $"/p:RuntimeFrameworkVersion={DockerRegistryManager.RuntimeFrameworkVersion}") + "/p:UseRazorSourceGenerator=false") .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) .WithWorkingDirectory(newProjectDir.FullName) .Execute(); @@ -668,8 +664,7 @@ public void EndToEnd_NoAPI_Console() $"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}", $"/p:ContainerRepository={imageName}", $"/p:ContainerImageTag={imageTag}", - "/p:EnableSdkContainerSupport=true", - $"/p:RuntimeFrameworkVersion={DockerRegistryManager.RuntimeFrameworkVersion}") + "/p:EnableSdkContainerSupport=true") .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) .WithWorkingDirectory(newProjectDir.FullName) .Execute() @@ -693,6 +688,485 @@ public void EndToEnd_NoAPI_Console() privateNuGetAssets.Delete(true); } + [DockerIsAvailableAndSupportsArchFact("linux/arm64")] + public void EndToEndMultiArch_LocalRegistry() + { + string imageName = NewImageName(); + string imageTag = "1.0"; + string imageX64 = $"{imageName}:{imageTag}-linux-x64"; + string imageArm64 = $"{imageName}:{imageTag}-linux-arm64"; + + // Create a new console project + DirectoryInfo newProjectDir = CreateNewProject("console"); + + // Run PublishContainer for multi-arch + CommandResult commandResult = new DotnetCommand( + _testOutput, + "build", + "/t:PublishContainer", + "/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + "/p:EnableSdkContainerSupport=true") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute(); + + // Check that the app was published for each RID, + // images were created locally for each RID + // and image index was NOT created + commandResult.Should().Pass() + .And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-x64")) + .And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-arm64")) + .And.HaveStdOutContaining($"Pushed image '{imageX64}' to local registry") + .And.HaveStdOutContaining($"Pushed image '{imageArm64}' to local registry") + .And.NotHaveStdOutContaining("Pushed image index"); + + // Check that the containers can be run + CommandResult processResultX64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + $"test-container-{imageName}-x64", + imageX64) + .Execute(); + processResultX64.Should().Pass().And.HaveStdOut("Hello, World!"); + + CommandResult processResultArm64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + $"test-container-{imageName}-arm64", + imageArm64) + .Execute(); + processResultArm64.Should().Pass().And.HaveStdOut("Hello, World!"); + + // Cleanup + newProjectDir.Delete(true); + } + + private DirectoryInfo CreateNewProject(string template, [CallerMemberName] string callerMemberName = "") + { + DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, callerMemberName)); + + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + + newProjectDir.Create(); + + new DotnetNewCommand(_testOutput, template, "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + return newProjectDir; + } + + private string GetPublishArtifactsPath(string projectDir, string rid) + => Path.Combine(projectDir, "bin", "Debug", ToolsetInfo.CurrentTargetFramework, rid, "publish"); + + [DockerIsAvailableAndSupportsArchFact("linux/arm64")] + public void EndToEndMultiArch_ArchivePublishing() + { + string imageName = NewImageName(); + string imageTag = "1.0"; + string imageX64 = $"{imageName}:{imageTag}-linux-x64"; + string imageArm64 = $"{imageName}:{imageTag}-linux-arm64"; + string archiveOutput = Path.Combine(TestSettings.TestArtifactsDirectory, "tarballs-output"); + string imageX64Tarball = Path.Combine(archiveOutput, $"{imageName}-linux-x64.tar.gz"); + string imageArm64Tarball = Path.Combine(archiveOutput, $"{imageName}-linux-arm64.tar.gz"); + + // Create a new console project + DirectoryInfo newProjectDir = CreateNewProject("console"); + + // Run PublishContainer for multi-arch with ContainerArchiveOutputPath + CommandResult commandResult = new DotnetCommand( + _testOutput, + "build", + "/t:PublishContainer", + "/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"", + $"/p:ContainerArchiveOutputPath={archiveOutput}", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + "/p:EnableSdkContainerSupport=true") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute(); + + // Check that the app was published for each RID, + // images were created locally for each RID + // and image index was NOT created + commandResult.Should().Pass() + .And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-x64")) + .And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-arm64")) + .And.HaveStdOutContaining($"Pushed image '{imageX64}' to local archive at '{imageX64Tarball}'") + .And.HaveStdOutContaining($"Pushed image '{imageArm64}' to local archive at '{imageArm64Tarball}'") + .And.NotHaveStdOutContaining("Pushed image index"); + + // Check that tarballs were created + File.Exists(imageX64Tarball).Should().BeTrue(); + File.Exists(imageArm64Tarball).Should().BeTrue(); + + // Load the images from the tarballs + ContainerCli.LoadCommand(_testOutput, "--input", imageX64Tarball) + .Execute() + .Should().Pass(); + ContainerCli.LoadCommand(_testOutput, "--input", imageArm64Tarball) + .Execute() + .Should().Pass(); + + // Check that the containers can be run + CommandResult processResultX64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + $"test-container-{imageName}-x64", + imageX64) + .Execute(); + processResultX64.Should().Pass().And.HaveStdOut("Hello, World!"); + + CommandResult processResultArm64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + $"test-container-{imageName}-arm64", + imageArm64) + .Execute(); + processResultArm64.Should().Pass().And.HaveStdOut("Hello, World!"); + + // Cleanup + newProjectDir.Delete(true); + } + + [DockerIsAvailableAndSupportsArchFact("linux/arm64")] + public void EndToEndMultiArch_RemoteRegistry() + { + string imageName = NewImageName(); + string imageTag = "1.0"; + string registry = DockerRegistryManager.LocalRegistry; + string imageX64 = $"{imageName}:{imageTag}-linux-x64"; + string imageArm64 = $"{imageName}:{imageTag}-linux-arm64"; + string imageIndex = $"{imageName}:{imageTag}"; + + // Create a new console project + DirectoryInfo newProjectDir = CreateNewProject("console"); + + // Run PublishContainer for multi-arch with ContainerRegistry + CommandResult commandResult = new DotnetCommand( + _testOutput, + "build", + "/t:PublishContainer", + "/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRegistry={registry}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + "/p:EnableSdkContainerSupport=true") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute(); + + // Check that the app was published for each RID, + // images were created locally for each RID + // and image index was created + commandResult.Should().Pass() + .And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-x64")) + .And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-arm64")) + .And.HaveStdOutContaining($"Pushed image '{imageX64}' to registry") + .And.HaveStdOutContaining($"Pushed image '{imageArm64}' to registry") + .And.HaveStdOutContaining($"Pushed image index '{imageIndex}' to registry '{registry}'"); + + + // Check that the containers can be run + // First pull the image from the registry, then tag so the image won't be overwritten + string imageX64Tagged = $"{registry}/test-image-{imageName}-x64"; + ContainerCli.PullCommand( + _testOutput, + "--platform", + "linux/amd64", + $"{registry}/{imageIndex}") + .Execute() + .Should().Pass(); + ContainerCli.TagCommand( + _testOutput, + $"{registry}/{imageIndex}", + imageX64Tagged) + .Execute() + .Should().Pass(); + CommandResult processResultX64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + $"test-container-{imageName}-x64", + imageX64Tagged) + .Execute(); + processResultX64.Should().Pass().And.HaveStdOut("Hello, World!"); + + string imageArm64Tagged = $"{registry}/test-image-{imageName}-arm64"; + ContainerCli.PullCommand( + _testOutput, + "--platform", + "linux/arm64", + $"{registry}/{imageIndex}") + .Execute() + .Should().Pass(); + ContainerCli.TagCommand( + _testOutput, + $"{registry}/{imageIndex}", + imageArm64Tagged) + .Execute() + .Should().Pass(); + CommandResult processResultArm64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + $"test-container-{imageName}-arm64", + imageArm64Tagged) + .Execute(); + processResultArm64.Should().Pass().And.HaveStdOut("Hello, World!"); + + // Cleanup + newProjectDir.Delete(true); + } + + [DockerAvailableFact] + public void EndToEndMultiArch_ContainerRuntimeIdentifiersOverridesRuntimeIdentifiers() + { + // Create a new console project + DirectoryInfo newProjectDir = CreateNewProject("console"); + string imageName = NewImageName(); + string imageTag = "1.0"; + + // Run PublishContainer for multi-arch with ContainerRuntimeIdentifiers + // RuntimeIdentifiers should contain all the RIDs from ContainerRuntimeIdentifiers to be able to publish + CommandResult commandResult = new DotnetCommand( + _testOutput, + "build", + "/t:PublishContainer", + "/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"", + "/p:ContainerRuntimeIdentifiers=linux-arm64", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + "/p:EnableSdkContainerSupport=true") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute(); + + // Check that the app was published only for RID from ContainerRuntimeIdentifiers + // images were created locally only for RID for from ContainerRuntimeIdentifiers + commandResult.Should().Pass() + .And.NotHaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-x64")) + .And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, "linux-arm64")) + .And.NotHaveStdOutContaining($"Pushed image '{imageName}:{imageTag}-linux-x64' to local registry") + .And.HaveStdOutContaining($"Pushed image '{imageName}:{imageTag}-linux-arm64' to local registry"); + + // Cleanup + newProjectDir.Delete(true); + } + + [DockerIsAvailableAndSupportsArchFact("linux/arm64")] + public void EndToEndMultiArch_EnvVariables() + { + string imageName = NewImageName(); + string imageTag = "1.0"; + string imageX64 = $"{imageName}:{imageTag}-linux-x64"; + string imageArm64 = $"{imageName}:{imageTag}-linux-arm64"; + + // Create new console app, set ContainerEnvironmentVariables, and set to output env variable + DirectoryInfo newProjectDir = CreateNewProject("console"); + var csprojPath = Path.Combine(newProjectDir.FullName, $"{nameof(EndToEndMultiArch_EnvVariables)}.csproj"); + var csprojContent = File.ReadAllText(csprojPath); + csprojContent = csprojContent.Replace("", + """ + + + + + + """); + File.WriteAllText(csprojPath, csprojContent); + File.WriteAllText(Path.Combine(newProjectDir.FullName, "Program.cs"), + """ + Console.Write(Environment.GetEnvironmentVariable("GoodEnvVar")); + Console.Write(Environment.GetEnvironmentVariable("AnotherEnvVar")); + """); + + // Run PublishContainer for multi-arch + new DotnetCommand( + _testOutput, + "build", + "/t:PublishContainer", + "/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + "/p:EnableSdkContainerSupport=true") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + // Check that the env var is printed + string containerNameX64 = $"test-container-{imageName}-x64"; + CommandResult processResultX64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + containerNameX64, + imageX64) + .Execute(); + processResultX64.Should().Pass().And.HaveStdOut("FooBar"); + + // Check that the env var is printed + string containerNameArm64 = $"test-container-{imageName}-arm64"; + CommandResult processResultArm64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + containerNameArm64, + imageArm64) + .Execute(); + processResultArm64.Should().Pass().And.HaveStdOut("FooBar"); + + // Cleanup + newProjectDir.Delete(true); + } + + [DockerIsAvailableAndSupportsArchFact("linux/arm64")] + public void EndToEndMultiArch_Ports() + { + string imageName = NewImageName(); + string imageTag = "1.0"; + string imageX64 = $"{imageName}:{imageTag}-linux-x64"; + string imageArm64 = $"{imageName}:{imageTag}-linux-arm64"; + + // Create new web app, set ContainerPort + DirectoryInfo newProjectDir = CreateNewProject("webapp"); + var csprojPath = Path.Combine(newProjectDir.FullName, $"{nameof(EndToEndMultiArch_Ports)}.csproj"); + var csprojContent = File.ReadAllText(csprojPath); + csprojContent = csprojContent.Replace("", + """ + + + + + + """); + File.WriteAllText(csprojPath, csprojContent); + + // Run PublishContainer for multi-arch + new DotnetCommand( + _testOutput, + "build", + "/t:PublishContainer", + "/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + "/p:EnableSdkContainerSupport=true") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + // Check that the ports are correct + var containerNameX64 = $"test-container-{imageName}-x64"; + CommandResult processResultX64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + containerNameX64, + "-P", + "--detach", + imageX64) + .Execute(); + processResultX64.Should().Pass(); + + // 8080 is the default port + CheckPorts(containerNameX64, [8080, 8082, 8083], [8081]); + + // Check that the ports are correct + var containerNameArm64 = $"test-container-{imageName}-arm64"; + CommandResult processResultArm64 = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + containerNameArm64, + "-P", + "--detach", + imageArm64) + .Execute(); + processResultArm64.Should().Pass(); + + // 8080 is the default port + CheckPorts(containerNameArm64, [8080, 8082, 8083], [8081]); + + // Cleanup + // we ran containers with detached option, so we need to stop them + ContainerCli.StopCommand(_testOutput, containerNameX64) + .Execute() + .Should().Pass(); + ContainerCli.StopCommand(_testOutput, containerNameArm64) + .Execute() + .Should().Pass(); + newProjectDir.Delete(true); + } + + private void CheckPorts(string containerName, int[] correctPorts, int[] incorrectPorts) + { + foreach (var port in correctPorts) + { + // Check the provided port is available + ContainerCli.PortCommand(_testOutput, containerName, port) + .Execute().Should().Pass(); + } + foreach (var port in incorrectPorts) + { + // Check that not provided port is not available + ContainerCli.PortCommand(_testOutput, containerName, port) + .Execute().Should().Fail(); + } + } + + [DockerAvailableFact] + public void EndToEndMultiArch_Labels() + { + string imageName = NewImageName(); + string imageTag = "1.0"; + string imageX64 = $"{imageName}:{imageTag}-linux-x64"; + + // Create new console app + DirectoryInfo newProjectDir = CreateNewProject("webapp"); + + // Run PublishContainer for multi-arch with ContainerGenerateLabels + new DotnetCommand( + _testOutput, + "build", + "/t:PublishContainer", + "/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + "/p:EnableSdkContainerSupport=true") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + // Check that labels are set + CommandResult inspectResult = ContainerCli.InspectCommand( + _testOutput, + "--format={{json .Config.Labels}}", + imageX64) + .Execute(); + inspectResult.Should().Pass(); + var labels = JsonSerializer.Deserialize>(inspectResult.StdOut); + labels.Should().NotBeNull().And.HaveCountGreaterThan(0); + labels!.Values.Should().AllSatisfy(value => value.Should().NotBeNullOrEmpty()); + + // Cleanup + newProjectDir.Delete(true); + } + [DockerSupportsArchInlineData("linux/arm/v7", "linux-arm", "/app")] [DockerSupportsArchInlineData("linux/arm64/v8", "linux-arm64", "/app")] [DockerSupportsArchInlineData("linux/386", "linux-x86", "/app", Skip = "There's no apphost for linux-x86 so we can't execute self-contained, and there's no .NET runtime base image for linux-x86 so we can't execute framework-dependent.")] @@ -709,7 +1183,7 @@ public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, var isWin = rid.StartsWith("win"); ImageBuilder? imageBuilder = await registry.GetImageManifestAsync( DockerRegistryManager.RuntimeBaseImage, - isWin ? DockerRegistryManager.Net8PreviewWindowsSpecificImageTag : DockerRegistryManager.Net9PreviewImageTag, + isWin ? DockerRegistryManager.Net8PreviewWindowsSpecificImageTag : DockerRegistryManager.Net9ImageTag, rid, ToolsetUtils.RidGraphManifestPicker, cancellationToken: default).ConfigureAwait(false); @@ -726,7 +1200,7 @@ public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { rid }); await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs index abb4d2b04559..88518f779397 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs @@ -387,6 +387,39 @@ public void MuslRidsGetAlpineContainers(string tfm, string rid, string expectedI computedBaseImageTag.Should().BeEquivalentTo(expectedImage); } + [InlineData("linux-musl-x64;linux-musl-arm64", "mcr.microsoft.com/dotnet/runtime:8.0-alpine")] + [InlineData("linux-x64;linux-arm64", "mcr.microsoft.com/dotnet/runtime:8.0")] + [Theory] + public void AllMuslRidsGetAlpineContainers(string rids, string expectedImage) + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["NetCoreSdkVersion"] = "8.0.100", + ["TargetFrameworkVersion"] = "v8.0", + [KnownStrings.Properties.ContainerRuntimeIdentifier] = rids, + }, projectName: $"{nameof(AllMuslRidsGetAlpineContainers)}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[] { ComputeContainerBaseImage }, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors)); + var computedBaseImageTag = instance.GetProperty(ContainerBaseImage)?.EvaluatedValue; + computedBaseImageTag.Should().BeEquivalentTo(expectedImage); + } + + [Fact] + public void NotAllMuslRidsLogsError() + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["NetCoreSdkVersion"] = "8.0.100", + ["TargetFrameworkVersion"] = "v8.0", + [KnownStrings.Properties.ContainerRuntimeIdentifier] = "linux-musl-x64;linux-arm64", + }, projectName: $"{nameof(NotAllMuslRidsLogsError)}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[] { ComputeContainerBaseImage }, [logger], null, out var outputs).Should().BeFalse(String.Join(Environment.NewLine, logger.Errors)); + logger.Errors.Should().ContainSingle(error => error.Message == Resources.Strings.InvalidTargetRuntimeIdentifiers); + } + [InlineData("linux-musl-x64", "mcr.microsoft.com/dotnet/nightly/runtime-deps:8.0-alpine-aot")] [InlineData("linux-x64", "mcr.microsoft.com/dotnet/nightly/runtime-deps:8.0-jammy-chiseled-aot")] [Theory] diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs b/test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs new file mode 100644 index 000000000000..ef6e5bb84b2a --- /dev/null +++ b/test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.NET.Build.Containers.Resources; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class ImageIndexGeneratorTests +{ + [Fact] + public void ImagesCannotBeEmpty() + { + ImageInfo[] images = Array.Empty(); + var ex = Assert.Throws(() => ImageIndexGenerator.GenerateImageIndex(images)); + Assert.Equal(Strings.ImagesEmpty, ex.Message); + } + + [Fact] + public void UnsupportedMediaTypeThrows() + { + ImageInfo[] images = new ImageInfo[] + { + new ImageInfo + { + ManifestMediaType = "unsupported" + } + }; + + var ex = Assert.Throws(() => ImageIndexGenerator.GenerateImageIndex(images)); + Assert.Equal(string.Format(Strings.UnsupportedMediaType, "unsupported"), ex.Message); + } + + [Theory] + [InlineData(SchemaTypes.DockerManifestV2)] + [InlineData(SchemaTypes.OciManifestV1)] + public void ConfigIsNotJsonObjectThrows(string supportedMediaType) + { + ImageInfo[] images = new ImageInfo[] + { + new ImageInfo + { + Config = "[]", + Manifest = "", + ManifestMediaType = supportedMediaType + } + }; + + var ex = Assert.Throws(() => ImageIndexGenerator.GenerateImageIndex(images)); + Assert.Equal($"Config should be a JSON object. (Parameter 'Config')", ex.Message); + } + + [Theory] + [InlineData(SchemaTypes.DockerManifestV2)] + [InlineData(SchemaTypes.OciManifestV1)] + public void ConfigDoesNotContainArchitectureThrows(string supportedMediaType) + { + ImageInfo[] images = new ImageInfo[] + { + new ImageInfo + { + Config = "{}", + Manifest = "", + ManifestMediaType = supportedMediaType + } + }; + + var ex = Assert.Throws(() => ImageIndexGenerator.GenerateImageIndex(images)); + Assert.Equal($"Config should contain 'architecture'. (Parameter 'Config')", ex.Message); + } + + [Theory] + [InlineData(SchemaTypes.DockerManifestV2)] + [InlineData(SchemaTypes.OciManifestV1)] + public void ConfigDoesNotContainOsThrows(string supportedMediaType) + { + ImageInfo[] images = new ImageInfo[] + { + new ImageInfo + { + Config = "{\"architecture\":\"arch1\"}", + Manifest = "", + ManifestMediaType = supportedMediaType + } + }; + + var ex = Assert.Throws(() => ImageIndexGenerator.GenerateImageIndex(images)); + Assert.Equal($"Config should contain 'os'. (Parameter 'Config')", ex.Message); + } + + [Theory] + [InlineData(SchemaTypes.DockerManifestV2)] + [InlineData(SchemaTypes.OciManifestV1)] + public void ImagesWithMixedMediaTypes(string supportedMediaType) + { + ImageInfo[] images = new ImageInfo[] + { + new ImageInfo + { + Config = "{\"architecture\":\"arch1\",\"os\":\"os1\"}", + Manifest = "", + ManifestMediaType = supportedMediaType + }, + new ImageInfo + { + Config = "", + Manifest = "", + ManifestMediaType = "anotherMediaType" + } + }; + + var ex = Assert.Throws(() => ImageIndexGenerator.GenerateImageIndex(images)); + Assert.Equal(Strings.MixedMediaTypes, ex.Message); + } + + [Fact] + public void GenerateDockerManifestList() + { + ImageInfo[] images = + [ + new ImageInfo + { + Config = "{\"architecture\":\"arch1\",\"os\":\"os1\"}", + ManifestDigest = "sha256:digest1", + Manifest = "123", + ManifestMediaType = SchemaTypes.DockerManifestV2 + }, + new ImageInfo + { + Config = "{\"architecture\":\"arch2\",\"os\":\"os2\"}", + ManifestDigest = "sha256:digest2", + Manifest = "123", + ManifestMediaType = SchemaTypes.DockerManifestV2 + } + ]; + + var (imageIndex, mediaType) = ImageIndexGenerator.GenerateImageIndex(images); + Assert.Equal("{\"schemaVersion\":2,\"mediaType\":\"application/vnd.docker.distribution.manifest.list.v2\\u002Bjson\",\"manifests\":[{\"mediaType\":\"application/vnd.docker.distribution.manifest.v2\\u002Bjson\",\"size\":3,\"digest\":\"sha256:digest1\",\"platform\":{\"architecture\":\"arch1\",\"os\":\"os1\",\"variant\":null,\"features\":null,\"os.version\":null}},{\"mediaType\":\"application/vnd.docker.distribution.manifest.v2\\u002Bjson\",\"size\":3,\"digest\":\"sha256:digest2\",\"platform\":{\"architecture\":\"arch2\",\"os\":\"os2\",\"variant\":null,\"features\":null,\"os.version\":null}}]}", imageIndex); + Assert.Equal(SchemaTypes.DockerManifestListV2, mediaType); + } + + [Fact] + public void GenerateOciImageIndex() + { + ImageInfo[] images = new ImageInfo[] + { + new ImageInfo + { + Config = "{\"architecture\":\"arch1\",\"os\":\"os1\"}", + ManifestDigest = "sha256:digest1", + Manifest = "123", + ManifestMediaType = SchemaTypes.OciManifestV1 + }, + new ImageInfo + { + Config = "{\"architecture\":\"arch2\",\"os\":\"os2\"}", + ManifestDigest = "sha256:digest2", + Manifest = "123", + ManifestMediaType = SchemaTypes.OciManifestV1 + } + }; + + var (imageIndex, mediaType) = ImageIndexGenerator.GenerateImageIndex(images); + Assert.Equal("{\"schemaVersion\":2,\"mediaType\":\"application/vnd.oci.image.index.v1\\u002Bjson\",\"manifests\":[{\"mediaType\":\"application/vnd.oci.image.manifest.v1\\u002Bjson\",\"size\":3,\"digest\":\"sha256:digest1\",\"platform\":{\"architecture\":\"arch1\",\"os\":\"os1\",\"variant\":null,\"features\":null,\"os.version\":null}},{\"mediaType\":\"application/vnd.oci.image.manifest.v1\\u002Bjson\",\"size\":3,\"digest\":\"sha256:digest2\",\"platform\":{\"architecture\":\"arch2\",\"os\":\"os2\",\"variant\":null,\"features\":null,\"os.version\":null}}]}", imageIndex); + Assert.Equal(SchemaTypes.OciImageIndexV1, mediaType); + } +}