diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs index 10e51d53666d..ee6b3b23e8d9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs @@ -14,6 +14,8 @@ namespace Microsoft.NET.Build.Containers; /// internal sealed class ImageBuilder { + internal const string BaseImageDigestName = "org.opencontainers.image.base.digest"; + // a snapshot of the manifest that this builder is based on private readonly ManifestV2 _baseImageManifest; @@ -21,6 +23,7 @@ internal sealed class ImageBuilder private readonly ManifestV2 _manifest; private readonly ImageConfig _baseImageConfig; private readonly ILogger _logger; + private Dictionary? _annotations; /// /// This is a parser for ASPNETCORE_URLS based on https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/BindingAddress.cs @@ -83,7 +86,8 @@ internal BuiltImage Build() Config = newManifestConfig, SchemaVersion = _manifest.SchemaVersion, MediaType = ManifestMediaType, - Layers = _manifest.Layers + Layers = _manifest.Layers, + Annotations = ManifestMediaType == SchemaTypes.OciManifestV1 ? _annotations : null }; return new BuiltImage() @@ -111,16 +115,34 @@ internal void AddLayer(Layer l) internal (string name, string value) AddBaseImageDigestLabel() { - var label = ("org.opencontainers.image.base.digest", _baseImageManifest.GetDigest()); + var label = (BaseImageDigestName, _baseImageManifest.GetDigest()); AddLabel(label.Item1, label.Item2); return label; } + internal void AddBaseImageDigestAnnotation() + { + string digest = _baseImageManifest.GetDigest(); + if (!string.IsNullOrEmpty(digest)) + { + AddAnnotation(BaseImageDigestName, digest); + } + } + /// /// Adds a label to a base image. /// internal void AddLabel(string name, string value) => _baseImageConfig.AddLabel(name, value); + /// + /// Adds an annotation to the generated OCI image manifest. + /// + internal void AddAnnotation(string name, string value) + { + _annotations ??= new(StringComparer.Ordinal); + _annotations[name] = value; + } + /// /// Adds environment variables to a base image. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs index 69403dc72dae..e5c66250eaa3 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs @@ -17,7 +17,7 @@ internal static class ImageIndexGenerator /// Returns json string of image index and image index mediaType. /// /// - internal static (string, string) GenerateImageIndex(BuiltImage[] images) + internal static (string, string) GenerateImageIndex(BuiltImage[] images, IReadOnlyDictionary? annotations = null) { if (images.Length == 0) { @@ -37,7 +37,7 @@ internal static (string, string) GenerateImageIndex(BuiltImage[] images) } else if (manifestMediaType == SchemaTypes.OciManifestV1) { - return (GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1), SchemaTypes.OciImageIndexV1); + return (GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1, annotations), SchemaTypes.OciImageIndexV1); } else { @@ -54,7 +54,7 @@ internal static (string, string) GenerateImageIndex(BuiltImage[] images) /// Returns json string of image index and image index mediaType. /// /// - internal static string GenerateImageIndex(BuiltImage[] images, string manifestMediaType, string imageIndexMediaType) + internal static string GenerateImageIndex(BuiltImage[] images, string manifestMediaType, string imageIndexMediaType, IReadOnlyDictionary? annotations = null) { if (images.Length == 0) { @@ -84,12 +84,28 @@ internal static string GenerateImageIndex(BuiltImage[] images, string manifestMe { schemaVersion = 2, mediaType = imageIndexMediaType, - manifests = manifests + manifests = manifests, + annotations = CopyAnnotations(annotations) }; return GetJsonStringFromImageIndex(imageIndex); } + private static Dictionary? CopyAnnotations(IReadOnlyDictionary? annotations) + { + if (annotations is not { Count: > 0 }) + { + return null; + } + + Dictionary result = new(annotations.Count, StringComparer.Ordinal); + foreach ((string key, string value) in annotations.OrderBy(annotation => annotation.Key, StringComparer.Ordinal)) + { + result[key] = value; + } + return result; + } + internal static string GenerateImageIndexWithAnnotations( string manifestMediaType, string manifestDigest, diff --git a/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs b/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs index 471f450f42d6..a002b27a1937 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs @@ -5,11 +5,17 @@ namespace Microsoft.NET.Build.Containers; -public record struct ManifestListV2(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests); +public record struct ManifestListV2(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests) +{ + public Dictionary? annotations { get; init; } +} public record struct PlatformInformation(string architecture, string os, string? variant, string[] features, [property: JsonPropertyName("os.version")][field: JsonPropertyName("os.version")] string? version); public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform); -public record struct ImageIndexV1(int schemaVersion, string mediaType, PlatformSpecificOciManifest[] manifests); +public record struct ImageIndexV1(int schemaVersion, string mediaType, PlatformSpecificOciManifest[] manifests) +{ + public Dictionary? annotations { get; init; } +} public record struct PlatformSpecificOciManifest(string mediaType, long size, string digest, PlatformInformation platform, Dictionary annotations); diff --git a/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs b/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs index a0403bdc035f..f158839e3c64 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs @@ -53,6 +53,13 @@ public class ManifestV2 [JsonPropertyName("layers")] public required List Layers { get; init; } + /// + /// Arbitrary metadata for this OCI image manifest. + /// + [JsonPropertyName("annotations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Annotations { get; init; } + /// /// Gets the digest for this manifest. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net11.0/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net11.0/PublicAPI.Unshipped.txt index 5db830dd4f46..f71346fdb148 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net11.0/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net11.0/PublicAPI.Unshipped.txt @@ -1,4 +1,15 @@ const Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.Docker = "Docker" -> string! +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.Annotations.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.Annotations.set -> void +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.ConvertContainerAnnotations() -> void +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.DecodedAnnotations.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.DecodedAnnotations.set -> void +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.EncodedAnnotations.get -> string! +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.EncodedAnnotations.set -> void +override Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.Execute() -> bool +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.SerializedAnnotations.get -> string! +Microsoft.NET.Build.Containers.Tasks.ConvertContainerAnnotations.SerializedAnnotations.set -> void const Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.Podman = "Podman" -> string! const Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.Wslc = "Wslc" -> string! const Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.MacOSContainer = "MacOSContainer" -> string! @@ -31,6 +42,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.BaseImageDigest.get -> str Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.BaseImageDigest.set -> void Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.ArchiveOutputPath.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.ArchiveOutputPath.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.Annotations.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.Annotations.set -> void Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.LocalRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.LocalRegistry.set -> void Microsoft.NET.Build.Containers.Tasks.CreateImageIndex.GeneratedArchiveOutputPath.get -> string! @@ -96,6 +109,8 @@ Microsoft.NET.Build.Containers.ManifestLayer.urls.get -> string![]? Microsoft.NET.Build.Containers.ManifestLayer.urls.set -> void Microsoft.NET.Build.Containers.ManifestListV2 Microsoft.NET.Build.Containers.ManifestListV2.ManifestListV2() -> void +Microsoft.NET.Build.Containers.ManifestListV2.annotations.get -> System.Collections.Generic.Dictionary? +Microsoft.NET.Build.Containers.ManifestListV2.annotations.init -> void Microsoft.NET.Build.Containers.ManifestListV2.ManifestListV2(int schemaVersion, string! mediaType, Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void Microsoft.NET.Build.Containers.ManifestListV2.manifests.get -> Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! Microsoft.NET.Build.Containers.ManifestListV2.manifests.set -> void @@ -105,6 +120,8 @@ Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.get -> int Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.set -> void Microsoft.NET.Build.Containers.ImageIndexV1 Microsoft.NET.Build.Containers.ImageIndexV1.ImageIndexV1() -> void +Microsoft.NET.Build.Containers.ImageIndexV1.annotations.get -> System.Collections.Generic.Dictionary? +Microsoft.NET.Build.Containers.ImageIndexV1.annotations.init -> void Microsoft.NET.Build.Containers.ImageIndexV1.ImageIndexV1(int schemaVersion, string! mediaType, Microsoft.NET.Build.Containers.PlatformSpecificOciManifest[]! manifests) -> void Microsoft.NET.Build.Containers.ImageIndexV1.manifests.get -> Microsoft.NET.Build.Containers.PlatformSpecificOciManifest[]! Microsoft.NET.Build.Containers.ImageIndexV1.manifests.set -> void @@ -114,6 +131,8 @@ Microsoft.NET.Build.Containers.ImageIndexV1.schemaVersion.get -> int Microsoft.NET.Build.Containers.ImageIndexV1.schemaVersion.set -> void Microsoft.NET.Build.Containers.ManifestV2 Microsoft.NET.Build.Containers.ManifestV2.Config.get -> Microsoft.NET.Build.Containers.ManifestConfig +Microsoft.NET.Build.Containers.ManifestV2.Annotations.get -> System.Collections.Generic.Dictionary? +Microsoft.NET.Build.Containers.ManifestV2.Annotations.init -> void Microsoft.NET.Build.Containers.ManifestV2.Config.init -> void Microsoft.NET.Build.Containers.ManifestV2.GetDigest() -> string! Microsoft.NET.Build.Containers.ManifestV2.KnownDigest.get -> string? @@ -235,6 +254,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageTags.get -> string![]! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageTags.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Labels.get -> Microsoft.Build.Framework.ITaskItem![]! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Labels.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Annotations.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Annotations.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.LocalRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.LocalRegistry.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.OutputRegistry.get -> string! diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx index 7a9cce993870..abe559de76a3 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx @@ -505,4 +505,13 @@ CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. {StrBegins="CONTAINER2031: "} + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + 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 ff1f27d3cf6e..498300a6b341 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 @@ -204,6 +204,11 @@ Index image nelze vytvořit, protože nebyly zadány žádné obrázky. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: '{0}' formátu kontejnerové image se nepodporuje. Podporované formáty jsou '{1}'. @@ -324,6 +334,11 @@ Server procesu démon ohlásil chyby: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: Nelze stáhnout vrstvu s popisovačem '{0}' z registru '{1}', protože neexistuje. 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 88ad45053418..5291b243a72b 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 @@ -204,6 +204,11 @@ Der Imageindex kann nicht erstellt werden, da keine Bilder bereitgestellt wurden. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: Das Containerimageformat '{0}' wird nicht unterstützt. Unterstützte Formate sind '{1}'. @@ -324,6 +334,11 @@ Vom Daemonserver wurden Fehler gemeldet: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: Die Ebene mit dem Deskriptor „{0}“ kann nicht aus der Registrierung „{1}“ heruntergeladen werden, da sie nicht vorhanden ist. 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 0273d84cdfef..61d279bfb2ca 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 @@ -204,6 +204,11 @@ No se puede crear el índice de imágenes porque no se proporcionó ninguna imagen. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: no se admite el formato de imagen de contenedor '{0}'. Los formatos admitidos son '{1}'. @@ -324,6 +334,11 @@ El servidor demonio notificó los errores: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: No se puede descargar la capa con el descriptor "{0}" del registro "{1}" porque no existe. 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 db5e696eb703..d7b755f5fa40 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 @@ -204,6 +204,11 @@ Impossible de créer l’index d’image, car aucune image n’a été fournie. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: le format d’image conteneur «{0}» n’est pas pris en charge. Les formats pris en charge sont « {1} ». @@ -324,6 +334,11 @@ Le serveur démon a signalé des erreurs : {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: impossible de télécharger la couche avec le descripteur '{0}' à partir du Registre '{1}', car elle n’existe pas. 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 2e2a3c1b7e0d..f0d64311e9dc 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 @@ -204,6 +204,11 @@ Non è possibile creare l'indice dell'immagine perché non sono state specificate immagini. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: il formato dell'immagine contenitore '{0}' non è supportato. I formati supportati sono '{1}'. @@ -324,6 +334,11 @@ Errori segnalati dal server daemon: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: non è possibile scaricare il livello con descrittore '{0}' dal registro '{1}' perché non esiste. 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 7f089486310a..85c7e59784b6 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 @@ -204,6 +204,11 @@ イメージ インデックスを作成できません。イメージが指定されていません。 + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: コンテナー イメージ形式 '{0}' はサポートされていません。サポートされている形式は '{1}' です。 @@ -324,6 +334,11 @@ デーモン サーバーからエラーが報告されました: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: レジストリ '{1}' 内の記述子 '{0}' を持つレイヤーは存在しないため、ダウンロードできません。 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 0ed455d0ce4d..55ec6772e2a7 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 @@ -204,6 +204,11 @@ 이미지가 제공되지 않았으므로 이미지 인덱스를 만들 수 없습니다. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: 컨테이너 이미지 형식 '{0}' 지원되지 않습니다. 지원되는 형식은 '{1}'. @@ -324,6 +334,11 @@ 디먼 서버에서 오류를 보고했습니다. {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: '{1}' 레지스트리에서 설명자가 '{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 ad454aff8dff..f58cea5616e1 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 @@ -204,6 +204,11 @@ Nie można utworzyć indeksu obrazu, ponieważ nie podano obrazów. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: '{0}' formatu obrazu kontenera nie jest obsługiwany. Obsługiwane formaty są '{1}'. @@ -324,6 +334,11 @@ Serwer demona zgłosił błędy: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: nie można pobrać warstwy o deskryptorze „{0}” z rejestru „{1}”, ponieważ nie istnieje. 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 b59b874d1e68..dfd43e278451 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 @@ -204,6 +204,11 @@ Não é possível criar índice de imagem porque nenhuma imagem foi fornecida. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: não há suporte para o formato '{0}' imagem do contêiner. Os formatos com suporte são '{1}'. @@ -324,6 +334,11 @@ O servidor daemon relatou erros: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: Não foi possível baixar a camada com o descritor '{0}' do registro '{1}' porque não existe. 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 855b66d3cbe0..2e98797a6892 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 @@ -204,6 +204,11 @@ Не удается создать индекс образа, так как изображения не предоставлены. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: формат '{0}' контейнера не поддерживается. Поддерживаемые форматы '{1}'. @@ -324,6 +334,11 @@ Сервер управляющей программы сообщил об ошибках: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: не удается скачать слой с дескриптором "{0}" из реестра "{1}", так как он не существует. 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 6094698e8978..b052fd29fc94 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 @@ -204,6 +204,11 @@ Görüntü dizini oluşturulamıyor çünkü görüntü sağlanmadı. + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: Kapsayıcı görüntü '{0}' biçimi desteklenmiyor. Desteklenen biçimler '{1}'. @@ -324,6 +334,11 @@ Daemon sunucusu hatalar bildirdi: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: '{0}' tanımlayıcısına sahip katman mevcut olmadığından '{1}' kayıt defterinden indirilemedi. 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 2a542be24d20..eca309a28912 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 @@ -204,6 +204,11 @@ 无法创建映像索引,因为未提供映像。 + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: 不支持容器映像格式“{0}”。支持的格式为“{1}”。 @@ -324,6 +334,11 @@ 守护程序服务器报告错误: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: 无法从注册表“{1}”下载描述符为“{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 76cd198a6152..8cfc56399aaf 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 @@ -204,6 +204,11 @@ 無法建立映射索引,因為未提供影像。 + + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + Index-scoped container annotations cannot be written to Docker manifest lists. Use the OCI image format or remove the applicable annotations. + + bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) bearer realm '{0}' uses scheme '{1}' which is not allowed; only 'https' is accepted (or 'http' when the registry is configured as insecure) @@ -219,6 +224,11 @@ bearer realm '{0}' is not a valid absolute URI Reason fragment included in InvalidRegistryAuthResponse. {0} is the raw realm value. + + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + Container annotation '{0}' has invalid Scope '{1}'. Valid values are Manifest, Index, or Manifest,Index. + + CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. CONTAINER2031: 不支援容器映像格式 '{0}'。支援的格式為 '{1}'。 @@ -324,6 +334,11 @@ 精靈伺服器報告錯誤: {0} {0} are the list of messages, each message starts with new line + + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + Manifest-scoped container annotations cannot be written to Docker image manifests. Use the OCI image format or remove the applicable annotations. + + CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: 無法從登錄 '{1}' 下載描述元為 '{0}' 的層,因為它不存在。 diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ContainerAnnotationScope.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ContainerAnnotationScope.cs new file mode 100644 index 000000000000..d8c28797589f --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ContainerAnnotationScope.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Build.Containers.Resources; + +namespace Microsoft.NET.Build.Containers.Tasks; + +[Flags] +internal enum ContainerAnnotationScope +{ + Manifest = 1, + Index = 2, +} + +internal static class ContainerAnnotationScopes +{ + internal static bool TryFilter(ITaskItem[] annotations, ContainerAnnotationScope requestedScope, TaskLoggingHelper log, out ITaskItem[] filtered) + { + List result = new(annotations.Length); + foreach (ITaskItem annotation in annotations) + { + string scopeMetadata = annotation.GetMetadata("Scope"); + ContainerAnnotationScope scope = requestedScope; + + if (!string.IsNullOrWhiteSpace(scopeMetadata)) + { + scope = 0; + foreach (string value in scopeMetadata.Split(',')) + { + if (value.Trim().Equals(nameof(ContainerAnnotationScope.Manifest), StringComparison.OrdinalIgnoreCase)) + { + scope |= ContainerAnnotationScope.Manifest; + } + else if (value.Trim().Equals(nameof(ContainerAnnotationScope.Index), StringComparison.OrdinalIgnoreCase)) + { + scope |= ContainerAnnotationScope.Index; + } + else + { + log.LogError(string.Format(Resource.GetString("InvalidContainerAnnotationScope"), annotation.ItemSpec, scopeMetadata)); + filtered = Array.Empty(); + return false; + } + } + } + + if ((scope & requestedScope) != 0) + { + result.Add(annotation); + } + } + + filtered = result.ToArray(); + return true; + } +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ConvertContainerAnnotations.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ConvertContainerAnnotations.cs new file mode 100644 index 000000000000..68128d3cb9ff --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ConvertContainerAnnotations.cs @@ -0,0 +1,55 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Build.Containers.Tasks; + +public sealed class ConvertContainerAnnotations : Microsoft.Build.Utilities.Task +{ + public ITaskItem[] Annotations { get; set; } = Array.Empty(); + + public string SerializedAnnotations { get; set; } = string.Empty; + + [Output] + public string EncodedAnnotations { get; set; } = string.Empty; + + [Output] + public ITaskItem[] DecodedAnnotations { get; set; } = Array.Empty(); + + public override bool Execute() + { + try + { + if (!string.IsNullOrEmpty(SerializedAnnotations)) + { + AnnotationData[] data = JsonSerializer.Deserialize(Convert.FromBase64String(SerializedAnnotations)) ?? Array.Empty(); + DecodedAnnotations = data.Select(annotation => + { + TaskItem item = new(annotation.Identity); + item.SetMetadata("Scope", annotation.Scope); + item.SetMetadata("Value", annotation.Value); + return (ITaskItem)item; + }).ToArray(); + } + else + { + AnnotationData[] data = Annotations.Select(annotation => new AnnotationData( + annotation.ItemSpec, + annotation.GetMetadata("Scope"), + annotation.GetMetadata("Value"))).ToArray(); + EncodedAnnotations = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(data)); + } + } + catch (Exception exception) when (exception is FormatException or JsonException) + { + Log.LogErrorFromException(exception, showStackTrace: false); + } + + return !Log.HasLoggedErrors; + } + + private sealed record AnnotationData(string Identity, string Scope, string Value); +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.Interface.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.Interface.cs index 538240841c6b..c748bda9a66a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.Interface.cs @@ -66,6 +66,12 @@ partial class CreateImageIndex [Required] public string[] ImageTags { get; set; } + /// + /// Annotations that the OCI image index will include in metadata. Items whose comma-separated Scope metadata includes Index are used; + /// for direct task callers, missing or empty Scope defaults to Index. + /// + public ITaskItem[] Annotations { get; set; } + /// /// The generated archive output path. /// @@ -90,9 +96,10 @@ public CreateImageIndex() LocalRegistry = string.Empty; Repository = string.Empty; ImageTags = Array.Empty(); + Annotations = Array.Empty(); GeneratedArchiveOutputPath = string.Empty; GeneratedImageIndex = string.Empty; TaskResources = Resource.Manager; } -} \ No newline at end of file +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs index 8046790069b7..a4951abba8bb 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs @@ -68,7 +68,23 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) return false; } - var multiArchImage = CreateMultiArchImage(images, destinationImageReference.Kind); + if (!ContainerAnnotationScopes.TryFilter(Annotations, ContainerAnnotationScope.Index, Log, out ITaskItem[] indexAnnotations)) + { + return false; + } + if (indexAnnotations.Length > 0 && destinationImageReference.Kind == DestinationImageReferenceKind.RemoteRegistry && + images.Any(image => image.ManifestMediaType == SchemaTypes.DockerManifestV2)) + { + Log.LogError(Resource.GetString("IndexAnnotationsRequireOci")); + return false; + } + + Dictionary annotations = new(StringComparer.Ordinal); + foreach (ITaskItem annotation in indexAnnotations) + { + annotations[annotation.ItemSpec] = annotation.GetMetadata("Value"); + } + var multiArchImage = CreateMultiArchImage(images, destinationImageReference.Kind, annotations); GeneratedImageIndex = multiArchImage.ImageIndex; GeneratedArchiveOutputPath = ArchiveOutputPath; @@ -163,7 +179,7 @@ private BuiltImage[] ParseImages(DestinationImageReferenceKind destinationKind) return (architecture, os); } - private static MultiArchImage CreateMultiArchImage(BuiltImage[] images, DestinationImageReferenceKind destinationImageKind) + private static MultiArchImage CreateMultiArchImage(BuiltImage[] images, DestinationImageReferenceKind destinationImageKind, IReadOnlyDictionary annotations) { switch (destinationImageKind) { @@ -171,12 +187,12 @@ private static MultiArchImage CreateMultiArchImage(BuiltImage[] images, Destinat return new MultiArchImage() { // For multi-arch we publish only oci-formatted image tarballs. - ImageIndex = ImageIndexGenerator.GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1), + ImageIndex = ImageIndexGenerator.GenerateImageIndex(images, SchemaTypes.OciManifestV1, SchemaTypes.OciImageIndexV1, annotations), ImageIndexMediaType = SchemaTypes.OciImageIndexV1, Images = images }; case DestinationImageReferenceKind.RemoteRegistry: - (string imageIndex, string mediaType) = ImageIndexGenerator.GenerateImageIndex(images); + (string imageIndex, string mediaType) = ImageIndexGenerator.GenerateImageIndex(images, annotations); return new MultiArchImage() { ImageIndex = imageIndex, 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 d33273ee309f..5d49e3a2a4f9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -116,6 +116,12 @@ partial class CreateNewImage /// public ITaskItem[] Labels { get; set; } + /// + /// Annotations that the OCI image manifest will include in metadata. Items whose comma-separated Scope metadata includes Manifest are used; + /// for direct task callers, missing or empty Scope defaults to Manifest. + /// + public ITaskItem[] Annotations { get; set; } + /// /// Container environment variables to set. /// @@ -206,6 +212,7 @@ public CreateNewImage() AppCommandArgs = Array.Empty(); AppCommandInstruction = ""; Labels = Array.Empty(); + Annotations = Array.Empty(); ExposedPorts = Array.Empty(); ContainerEnvironmentVariables = Array.Empty(); ContainerRuntimeIdentifier = ""; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index 64506e88b352..4920518c0be0 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -94,6 +94,11 @@ private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuild var telemetry = new Telemetry(sourceImageReference, destinationImageReference, Log); + if (!ContainerAnnotationScopes.TryFilter(Annotations, ContainerAnnotationScope.Manifest, Log, out ITaskItem[] manifestAnnotations)) + { + return false; + } + ImageBuilder? imageBuilder; if (sourceRegistry is { } registry) { @@ -163,6 +168,13 @@ private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuild imageBuilder.ManifestMediaType, requestedImageFormat, destinationImageReference); + + if (manifestAnnotations.Length > 0 && imageBuilder.ManifestMediaType == SchemaTypes.DockerManifestV2) + { + Log.LogError(Resource.GetString("ManifestAnnotationsRequireOci")); + return false; + } + var userId = imageBuilder.IsWindows ? null : ContainerHelpers.TryParseUserId(ContainerUser); Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory, imageBuilder.IsWindows, imageBuilder.ManifestMediaType, userId); imageBuilder.AddLayer(newLayer); @@ -185,6 +197,7 @@ private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuild (baseImageLabel, baseImageDigest) = imageBuilder.AddBaseImageDigestLabel(); } } + else { if (GenerateDigestLabel) @@ -193,6 +206,19 @@ private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuild } } + foreach (ITaskItem annotation in manifestAnnotations) + { + string value = annotation.GetMetadata("Value"); + if (annotation.ItemSpec == ImageBuilder.BaseImageDigestName && string.IsNullOrEmpty(value)) + { + imageBuilder.AddBaseImageDigestAnnotation(); + } + else + { + imageBuilder.AddAnnotation(annotation.ItemSpec, value); + } + } + SetEnvironmentVariables(imageBuilder, ContainerEnvironmentVariables); SetPorts(imageBuilder, ExposedPorts); diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props index 7aca78125965..6903b1f17e4b 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 07f09c6aa9b8..d54010bc7cda 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets @@ -135,6 +135,23 @@ true true true + + true + false + $(ContainerGenerateLabelsImageCreated) + $(ContainerGenerateLabelsImageDescription) + $(ContainerGenerateLabelsImageAuthors) + $(ContainerGenerateLabelsImageUrl) + $(ContainerGenerateLabelsImageDocumentation) + $(ContainerGenerateLabelsImageSource) + $(ContainerGenerateLabelsImageVersion) + $(ContainerGenerateLabelsImageRevision) + $(ContainerGenerateLabelsImageVendor) + $(ContainerGenerateLabelsImageLicenses) + $(ContainerGenerateLabelsImageTitle) + $(ContainerGenerateLabelsImageBaseDigest) + $(ContainerGenerateLabelsImageBaseName) + $(ContainerGenerateLabelsDotnetToolset) @@ -145,11 +162,12 @@ $(PackageVersion) $(PackageLicenseExpression) $(Title) + <_ContainerImageCreated>$([System.DateTime]::UtcNow.ToString('o')) - + @@ -165,9 +183,27 @@ + + + + + + + + + + + + + + + + + + - + <_TrimmedRepositoryUrl Condition="'$(RepositoryType)' == 'git' and '$(PrivateRepositoryUrl)' != '' and $(PrivateRepositoryUrl.EndsWith('.git'))">$(PrivateRepositoryUrl.Substring(0, $(PrivateRepositoryUrl.LastIndexOf('.git')))) <_TrimmedRepositoryUrl Condition="'$(_TrimmedRepositoryUrl)' == '' and '$(PrivateRepositoryUrl)' != ''">$(PrivateRepositoryUrl) @@ -176,6 +212,24 @@ + + + + + + + + <_ManifestContainerAnnotation Include="@(ContainerAnnotation)" Condition="$([System.String]::Copy('%(ContainerAnnotation.Scope)').ToLowerInvariant().Contains('manifest'))" /> + <_IndexContainerAnnotation Include="@(ContainerAnnotation)" Condition="$([System.String]::Copy('%(ContainerAnnotation.Scope)').ToLowerInvariant().Contains('index'))" /> + <_ApplicableContainerAnnotation Include="@(_ManifestContainerAnnotation)" /> + <_ApplicableContainerAnnotation Include="@(_IndexContainerAnnotation)" Condition="'$(_IsMultiRIDBuild)' == 'true'" /> + + + OCI + + @@ -271,6 +325,7 @@ AppCommandInstruction="$(ContainerAppCommandInstruction)" DefaultArgs="@(ContainerDefaultArgs)" Labels="@(ContainerLabel)" + Annotations="@(ContainerAnnotation)" ExposedPorts="@(ContainerPort)" ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)" ContainerRuntimeIdentifier="$(ContainerRuntimeIdentifier)" @@ -313,6 +368,10 @@ <_SingleImageContainerFormat Condition="$(_SkipContainerPublishing) == 'true' ">OCI + + + + <_rids Include="$(ContainerRuntimeIdentifiers)" Condition="'$(ContainerRuntimeIdentifiers)' != ''" /> <_rids Include="$(RuntimeIdentifiers)" Condition="'$(ContainerRuntimeIdentifiers)' == '' and '$(RuntimeIdentifiers)' != ''" /> @@ -329,6 +388,7 @@ _ContainerImageTags=@(ContainerImageTags, ';'); ContainerRepository=$(ContainerRepository); _ContainerLabel=@(ContainerLabel->'%(Identity):%(Value)'); + _SerializedContainerAnnotations=$(_SerializedContainerAnnotations); _ContainerPort=@(ContainerPort->'%(Identity):%(Type)'); _ContainerEnvironmentVariables=@(ContainerEnvironmentVariable->'%(Identity):%(Value)'); ContainerGenerateLabels=$(ContainerGenerateLabels); @@ -356,6 +416,7 @@ ArchiveOutputPath="$(ContainerArchiveOutputPath)" Repository="$(ContainerRepository)" ImageTags="@(ContainerImageTags)" + Annotations="@(ContainerAnnotation)" BaseRegistry="$(ContainerBaseRegistry)" BaseImageName="$(ContainerBaseName)" BaseImageTag="$(ContainerBaseTag)" @@ -366,6 +427,9 @@ + + + diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs index 60a1737bcc01..c38c4c0d971e 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Runtime.CompilerServices; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; @@ -88,10 +89,9 @@ public static (Project, CapturingLogger, IDisposable) InitProject(Dictionary item.EvaluatedInclude == label && item.GetMetadata("Value") is { } v && v.EvaluatedValue == value; + private static bool AnnotationMatch(string annotation, string value, ProjectItemInstance item) => item.EvaluatedInclude == annotation && item.GetMetadata("Value") is { } v && v.EvaluatedValue == value; + + [TestMethod] + public void GetsConventionalAnnotationsFromAvailableMetadata() + { + const string repoUrl = "https://github.com/dotnet/sdk.git"; + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["PublishRepositoryUrl"] = true.ToString(), + ["PrivateRepositoryUrl"] = repoUrl, + ["RepositoryType"] = "git", + ["SourceRevisionId"] = "abcdef", + ["PackageVersion"] = "1.2.3", + ["Description"] = "SDK container", + ["ContainerGenerateAnnotations"] = true.ToString() + }, projectName: nameof(GetsConventionalAnnotationsFromAvailableMetadata)); + using var _ = d; + var instance = project.CreateProjectInstance(ProjectInstanceSettings.None); + + instance.Build([ComputeContainerConfig], [logger]).Should().BeTrue(String.Join(Environment.NewLine, logger.AllMessages)); + + var annotations = instance.GetItems("ContainerAnnotation"); + annotations.Should() + .ContainSingle(annotation => AnnotationMatch("org.opencontainers.image.source", "https://github.com/dotnet/sdk", annotation)) + .And.ContainSingle(annotation => AnnotationMatch("org.opencontainers.image.revision", "abcdef", annotation)) + .And.ContainSingle(annotation => AnnotationMatch("org.opencontainers.image.version", "1.2.3", annotation)) + .And.ContainSingle(annotation => AnnotationMatch("org.opencontainers.image.description", "SDK container", annotation)) + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "org.opencontainers.image.base.digest" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest") + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "org.opencontainers.image.base.name" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest") + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "net.dot.runtime.majorminor" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest") + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "net.dot.sdk.version" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest"); + annotations + .Where(annotation => !annotation.EvaluatedInclude.StartsWith("org.opencontainers.image.base.", StringComparison.Ordinal) && !annotation.EvaluatedInclude.StartsWith("net.dot.", StringComparison.Ordinal)) + .Should().OnlyContain(annotation => annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest,Index"); + instance.GetPropertyValue("ContainerImageFormat").Should().Be("OCI"); + } + + [DataRow("", true)] + [DataRow("false", false)] + [TestMethod] + public void OciFormatControlsDefaultAnnotationGeneration(string generateAnnotations, bool shouldGenerateAnnotations) + { + Dictionary properties = new() + { + ["ContainerImageFormat"] = "OCI", + ["Description"] = "SDK container" + }; + if (generateAnnotations.Length > 0) + { + properties["ContainerGenerateAnnotations"] = generateAnnotations; + } + + var (project, logger, d) = ProjectInitializer.InitProject(properties, + projectName: $"{nameof(OciFormatControlsDefaultAnnotationGeneration)}_{shouldGenerateAnnotations}"); + using var _ = d; + var instance = project.CreateProjectInstance(ProjectInstanceSettings.None); + + instance.Build([ComputeContainerConfig], [logger]).Should().BeTrue(String.Join(Environment.NewLine, logger.AllMessages)); + var annotations = instance.GetItems("ContainerAnnotation"); + if (shouldGenerateAnnotations) + { + annotations.Should() + .ContainSingle(annotation => annotation.EvaluatedInclude == "org.opencontainers.image.created" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest,Index") + .And.ContainSingle(annotation => AnnotationMatch("org.opencontainers.image.description", "SDK container", annotation) && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest,Index") + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "org.opencontainers.image.base.digest" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest") + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "org.opencontainers.image.base.name" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest") + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "net.dot.runtime.majorminor" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest") + .And.ContainSingle(annotation => annotation.EvaluatedInclude == "net.dot.sdk.version" && annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest"); + } + else + { + annotations.Should().BeEmpty(); + } + } + + [DataRow("", true, "OCI")] + [DataRow("Docker", false, "Docker")] + [DataRow("OCI", true, "OCI")] + [TestMethod] + public void ContainerAnnotationsRequireOciFormat(string requestedFormat, bool shouldSucceed, string expectedFormat) + { + var annotation = new Microsoft.Build.Utilities.TaskItem("example.com/annotation"); + annotation.SetMetadata("Value", "value"); + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["ContainerImageFormat"] = requestedFormat + }, bonusItems: new() + { + ["ContainerAnnotation"] = [annotation] + }, projectName: $"{nameof(ContainerAnnotationsRequireOciFormat)}_{requestedFormat}"); + using var _ = d; + var instance = project.CreateProjectInstance(ProjectInstanceSettings.None); + + instance.Build([ComputeContainerConfig], [logger]).Should().Be(shouldSucceed, String.Join(Environment.NewLine, logger.AllMessages)); + instance.GetItems("ContainerAnnotation").Should().ContainSingle(annotation => annotation.GetMetadata("Scope")!.EvaluatedValue == "Manifest"); + instance.GetPropertyValue("ContainerImageFormat").Should().Be(expectedFormat); + } + + [TestMethod] + public void WhitespaceAnnotationScopeDefaultsToManifest() + { + var annotation = new Microsoft.Build.Utilities.TaskItem("example.com/annotation"); + annotation.SetMetadata("Scope", " "); + annotation.SetMetadata("Value", "value"); + var (project, logger, d) = ProjectInitializer.InitProject([], bonusItems: new() + { + ["ContainerAnnotation"] = [annotation] + }, projectName: nameof(WhitespaceAnnotationScopeDefaultsToManifest)); + using var _ = d; + var instance = project.CreateProjectInstance(ProjectInstanceSettings.None); + + instance.Build([ComputeContainerConfig], [logger]).Should().BeTrue(String.Join(Environment.NewLine, logger.AllMessages)); + instance.GetItems("ContainerAnnotation").Should().ContainSingle(item => item.GetMetadata("Scope")!.EvaluatedValue == "Manifest"); + instance.GetPropertyValue("ContainerImageFormat").Should().Be("OCI"); + } + + [TestMethod] + public void IndexOnlyAnnotationDoesNotRequireOciForSinglePlatformImage() + { + var annotation = new Microsoft.Build.Utilities.TaskItem("example.com/annotation"); + annotation.SetMetadata("Scope", "Index"); + annotation.SetMetadata("Value", "value"); + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["ContainerImageFormat"] = "Docker" + }, bonusItems: new() + { + ["ContainerAnnotation"] = [annotation] + }, projectName: nameof(IndexOnlyAnnotationDoesNotRequireOciForSinglePlatformImage)); + using var _ = d; + var instance = project.CreateProjectInstance(ProjectInstanceSettings.None); + + instance.Build([ComputeContainerConfig], [logger]).Should().BeTrue(String.Join(Environment.NewLine, logger.AllMessages)); + instance.GetPropertyValue("ContainerImageFormat").Should().Be("Docker"); + } + [TestMethod] public void MultiArchContainerLabelsPreserveColonsInValues() { diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/ContainerAnnotationScopeTests.cs b/test/Microsoft.NET.Build.Containers.UnitTests/ContainerAnnotationScopeTests.cs new file mode 100644 index 000000000000..41c98799eeb9 --- /dev/null +++ b/test/Microsoft.NET.Build.Containers.UnitTests/ContainerAnnotationScopeTests.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Build.Containers.Tasks; +using Moq; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +[TestClass] +public class ContainerAnnotationScopeTests +{ + [TestMethod] + [DataRow(null, true, true)] + [DataRow("", true, true)] + [DataRow("Manifest", true, false)] + [DataRow(" index ", false, true)] + [DataRow(" INDEX, manifest ", true, true)] + public void FiltersScopes(string? scope, bool appliesToManifest, bool appliesToIndex) + { + TaskItem annotation = new("example.annotation"); + if (scope is not null) + { + annotation.SetMetadata("Scope", scope); + } + var task = new TestTask { BuildEngine = new Mock().Object }; + + Assert.IsTrue(ContainerAnnotationScopes.TryFilter([annotation], ContainerAnnotationScope.Manifest, task.Log, out ITaskItem[] manifests)); + Assert.HasCount(appliesToManifest ? 1 : 0, manifests); + Assert.IsTrue(ContainerAnnotationScopes.TryFilter([annotation], ContainerAnnotationScope.Index, task.Log, out ITaskItem[] indexes)); + Assert.HasCount(appliesToIndex ? 1 : 0, indexes); + } + + [TestMethod] + public void RejectsInvalidScope() + { + TaskItem annotation = new("example.annotation"); + annotation.SetMetadata("Scope", "Manifest,Descriptor"); + var task = new TestTask { BuildEngine = new Mock().Object }; + + Assert.IsFalse(ContainerAnnotationScopes.TryFilter([annotation], ContainerAnnotationScope.Manifest, task.Log, out _)); + Assert.IsTrue(task.Log.HasLoggedErrors); + } + + private sealed class TestTask : Microsoft.Build.Utilities.Task + { + public override bool Execute() => true; + } +} diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/ConvertContainerAnnotationsTests.cs b/test/Microsoft.NET.Build.Containers.UnitTests/ConvertContainerAnnotationsTests.cs new file mode 100644 index 000000000000..fbfbd55f16a8 --- /dev/null +++ b/test/Microsoft.NET.Build.Containers.UnitTests/ConvertContainerAnnotationsTests.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Build.Containers.Tasks; +using Moq; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +[TestClass] +public class ConvertContainerAnnotationsTests +{ + [TestMethod] + public void RoundTripsArbitraryAnnotationValues() + { + ITaskItem[] annotations = + [ + Create("first:semicolon", "Manifest,Index", "semi;colon:percent% apostrophe' backtick` dollar$ at@ parentheses()"), + Create("second", "Index", "multiple;:%%'$@`() values"), + ]; + var encoder = new ConvertContainerAnnotations + { + Annotations = annotations, + BuildEngine = new Mock().Object, + }; + Assert.IsTrue(encoder.Execute()); + + var decoder = new ConvertContainerAnnotations + { + SerializedAnnotations = encoder.EncodedAnnotations, + BuildEngine = new Mock().Object, + }; + Assert.IsTrue(decoder.Execute()); + Assert.HasCount(2, decoder.DecodedAnnotations); + for (int i = 0; i < annotations.Length; i++) + { + Assert.AreEqual(annotations[i].ItemSpec, decoder.DecodedAnnotations[i].ItemSpec); + Assert.AreEqual(annotations[i].GetMetadata("Scope"), decoder.DecodedAnnotations[i].GetMetadata("Scope")); + Assert.AreEqual(annotations[i].GetMetadata("Value"), decoder.DecodedAnnotations[i].GetMetadata("Value")); + } + } + + private static TaskItem Create(string identity, string scope, string value) + { + TaskItem item = new(identity); + item.SetMetadata("Scope", scope); + item.SetMetadata("Value", value); + return item; + } +} diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs b/test/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs index 849826edab2f..928bb26c73a0 100644 --- a/test/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs +++ b/test/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs @@ -94,6 +94,29 @@ public void CanAddLabelsToImage() Assert.AreEqual("v2", resultLabels["testLabel2"]?.ToString()); } + [TestMethod] + public void CanAddAnnotationsToOciImageManifest() + { + var builder = FromBaseImageConfig( + """ + { + "architecture": "amd64", + "config": {}, + "os": "linux", + "rootfs": { "type": "layers", "diff_ids": [] } + } + """); + builder.ManifestMediaType = SchemaTypes.OciManifestV1; + builder.AddAnnotation("org.opencontainers.image.source", "https://github.com/dotnet/sdk"); + builder.AddAnnotation("org.opencontainers.image.revision", "abcdef"); + + JsonNode? manifest = JsonNode.Parse(builder.Build().Manifest); + + Assert.IsNotNull(manifest); + Assert.AreEqual("https://github.com/dotnet/sdk", manifest["annotations"]?["org.opencontainers.image.source"]?.GetValue()); + Assert.AreEqual("abcdef", manifest["annotations"]?["org.opencontainers.image.revision"]?.GetValue()); + } + [TestMethod] public void CanPreserveExistingLabels() { @@ -695,7 +718,50 @@ public void CanSetBaseImageDigestLabel() Assert.AreEqual(StaticKnownDigestValue, digest.GetValue()); } - private ImageBuilder FromBaseImageConfig(string baseImageConfig, [CallerMemberName] string testName = "") + [TestMethod] + public void CanSetBaseImageDigestAnnotation() + { + var builder = FromBaseImageConfig( + """ + { + "architecture": "amd64", + "config": {}, + "os": "linux", + "rootfs": { "type": "layers", "diff_ids": [] } + } + """); + builder.ManifestMediaType = SchemaTypes.OciManifestV1; + + builder.AddBaseImageDigestAnnotation(); + + JsonNode? manifest = JsonNode.Parse(builder.Build().Manifest); + Assert.IsNotNull(manifest); + Assert.AreEqual(StaticKnownDigestValue, manifest["annotations"]?["org.opencontainers.image.base.digest"]?.GetValue()); + } + + [TestMethod] + public void DoesNotSetEmptyBaseImageDigestAnnotation() + { + var builder = FromBaseImageConfig( + """ + { + "architecture": "amd64", + "config": {}, + "os": "linux", + "rootfs": { "type": "layers", "diff_ids": [] } + } + """, + knownDigest: string.Empty); + builder.ManifestMediaType = SchemaTypes.OciManifestV1; + + builder.AddBaseImageDigestAnnotation(); + + JsonNode? manifest = JsonNode.Parse(builder.Build().Manifest); + Assert.IsNotNull(manifest); + Assert.IsNull(manifest["annotations"]); + } + + private ImageBuilder FromBaseImageConfig(string baseImageConfig, string? knownDigest = null, [CallerMemberName] string testName = "") { var manifest = new ManifestV2() { @@ -708,7 +774,7 @@ private ImageBuilder FromBaseImageConfig(string baseImageConfig, [CallerMemberNa digest = "sha256:" }, Layers = new List(), - KnownDigest = StaticKnownDigestValue + KnownDigest = knownDigest ?? StaticKnownDigestValue }; return new ImageBuilder(manifest, manifest.MediaType, new ImageConfig(baseImageConfig), _loggerFactory.CreateLogger(testName)); } diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs b/test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs index 8c64c3b8867e..6a40c41f5b0f 100644 --- a/test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs +++ b/test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs @@ -1,6 +1,7 @@ // 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 Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers.UnitTests; @@ -135,6 +136,77 @@ public void GenerateOciImageIndex() Assert.AreEqual(SchemaTypes.OciImageIndexV1, mediaType); } + [TestMethod] + public void GenerateOciImageIndexWithAnnotations() + { + BuiltImage[] images = + [ + new BuiltImage + { + Config = "", + Manifest = "123", + ManifestDigest = "sha256:digest1", + ManifestMediaType = SchemaTypes.OciManifestV1, + Architecture = "arch1", + OS = "os1" + } + ]; + Dictionary annotations = new() + { + ["org.opencontainers.image.source"] = "https://github.com/dotnet/sdk", + ["org.opencontainers.image.revision"] = "abcdef" + }; + Dictionary annotationsInReverseOrder = new() + { + ["org.opencontainers.image.revision"] = "abcdef", + ["org.opencontainers.image.source"] = "https://github.com/dotnet/sdk" + }; + + var (imageIndex, mediaType) = ImageIndexGenerator.GenerateImageIndex(images, annotations); + var (imageIndexFromReverseOrder, _) = ImageIndexGenerator.GenerateImageIndex(images, annotationsInReverseOrder); + + Assert.AreEqual(imageIndex, imageIndexFromReverseOrder); + Assert.AreEqual(SchemaTypes.OciImageIndexV1, mediaType); + + using JsonDocument document = JsonDocument.Parse(imageIndex); + JsonElement root = document.RootElement; + Assert.AreEqual(2, root.GetProperty("schemaVersion").GetInt32()); + Assert.AreEqual(SchemaTypes.OciImageIndexV1, root.GetProperty("mediaType").GetString()); + + JsonElement manifest = root.GetProperty("manifests")[0]; + Assert.AreEqual(SchemaTypes.OciManifestV1, manifest.GetProperty("mediaType").GetString()); + Assert.AreEqual(3, manifest.GetProperty("size").GetInt64()); + Assert.AreEqual("sha256:digest1", manifest.GetProperty("digest").GetString()); + Assert.AreEqual("arch1", manifest.GetProperty("platform").GetProperty("architecture").GetString()); + Assert.AreEqual("os1", manifest.GetProperty("platform").GetProperty("os").GetString()); + + JsonElement serializedAnnotations = root.GetProperty("annotations"); + Assert.HasCount(2, serializedAnnotations.EnumerateObject()); + Assert.AreEqual("https://github.com/dotnet/sdk", serializedAnnotations.GetProperty("org.opencontainers.image.source").GetString()); + Assert.AreEqual("abcdef", serializedAnnotations.GetProperty("org.opencontainers.image.revision").GetString()); + } + + [TestMethod] + public void DockerManifestListDoesNotIncludeOciAnnotations() + { + BuiltImage[] images = + [ + new BuiltImage + { + Config = "", + Manifest = "123", + ManifestDigest = "sha256:digest1", + ManifestMediaType = SchemaTypes.DockerManifestV2, + Architecture = "arch1", + OS = "os1" + } + ]; + + var (imageIndex, _) = ImageIndexGenerator.GenerateImageIndex(images, new Dictionary { ["example.com/key"] = "value" }); + + Assert.IsFalse(imageIndex.Contains("annotations", StringComparison.Ordinal)); + } + [TestMethod] public void GenerateImageIndexWithAnnotations() {