diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs b/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs index d6a9c56850ec..bb1cbf494412 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs @@ -12,6 +12,7 @@ public static async Task PublishImageAsync( BuiltImage singleArchImage, SourceImageReference sourceImageReference, DestinationImageReference destinationImageReference, + bool noCache, Microsoft.Build.Utilities.TaskLoggingHelper Log, Telemetry telemetry, CancellationToken cancellationToken) @@ -37,7 +38,12 @@ await PushToRemoteRegistryAsync( destinationImageReference, Log, cancellationToken, - destinationImageReference.RemoteRegistry!.PushAsync, + (image, source, destination, token) => destinationImageReference.RemoteRegistry!.PushAsync( + image, + source, + destination, + noCache, + token), Strings.ContainerBuilder_ImageUploadedToRegistry).ConfigureAwait(false); break; default: 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..d67c55dbbe76 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 @@ -253,6 +253,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.get -> b Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.SkipPublishing.get -> bool Microsoft.NET.Build.Containers.Tasks.CreateNewImage.SkipPublishing.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.NoCache.get -> bool +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.NoCache.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerNames.get -> Microsoft.Build.Framework.ITaskItem![]! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerNames.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageFormat.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 363df11b416d..1fccccd5e1b0 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs @@ -24,6 +24,19 @@ internal DefaultManifestOperations(Uri baseUri, string registryName, HttpClient _registryName = registryName; } + public async Task ExistsAsync(string repositoryName, string reference, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Head, new Uri(_baseUri, $"/v2/{repositoryName}/manifests/{reference}")).AcceptManifestFormats(); + using HttpResponseMessage response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false); + return response.StatusCode switch + { + HttpStatusCode.OK => true, + _ when (int)response.StatusCode >= 500 => await LogAndThrowContainerHttpException(response, cancellationToken).ConfigureAwait(false), + _ => false, + }; + } + public async Task GetAsync(string repositoryName, string reference, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs index 221e2fe594a9..d7f50fd6b195 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs @@ -11,6 +11,8 @@ namespace Microsoft.NET.Build.Containers; /// internal interface IManifestOperations { + public Task ExistsAsync(string repositoryName, string reference, CancellationToken cancellationToken); + public Task GetAsync(string repositoryName, string reference, 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 1b359e4d7886..2c4fb4eaacd6 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs @@ -595,13 +595,24 @@ public async Task PushManifestListAsync( } public Task PushAsync(BuiltImage builtImage, SourceImageReference source, DestinationImageReference destination, CancellationToken cancellationToken) - => PushAsync(builtImage, source, destination, pushTags: true, cancellationToken); + => PushAsync(builtImage, source, destination, noCache: false, cancellationToken); - private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, DestinationImageReference destination, bool pushTags, CancellationToken cancellationToken) + public Task PushAsync(BuiltImage builtImage, SourceImageReference source, DestinationImageReference destination, bool noCache, CancellationToken cancellationToken) + => PushAsync(builtImage, source, destination, pushTags: true, noCache, cancellationToken); + + private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, DestinationImageReference destination, bool pushTags, bool noCache, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Registry destinationRegistry = destination.RemoteRegistry!; + bool manifestExists = !noCache && + await _registryAPI.Manifest.ExistsAsync(destination.Repository, builtImage.ManifestDigest, cancellationToken).ConfigureAwait(false); + + if (manifestExists) + { + _logger.LogInformation(Strings.Registry_ManifestExists, builtImage.ManifestDigest, destination.Repository); + } + Func uploadLayerFunc = async (descriptor) => { cancellationToken.ThrowIfCancellationRequested(); @@ -634,25 +645,28 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, } }; - if (SupportsParallelUploads) + if (!manifestExists) { - await Task.WhenAll(builtImage.LayerDescriptors.Select(descriptor => uploadLayerFunc(descriptor))).ConfigureAwait(false); - } - else - { - foreach (var descriptor in builtImage.LayerDescriptors) + if (SupportsParallelUploads) { - await uploadLayerFunc(descriptor).ConfigureAwait(false); + await Task.WhenAll(builtImage.LayerDescriptors.Select(descriptor => uploadLayerFunc(descriptor))).ConfigureAwait(false); + } + else + { + foreach (var descriptor in builtImage.LayerDescriptors) + { + await uploadLayerFunc(descriptor).ConfigureAwait(false); + } } - } - cancellationToken.ThrowIfCancellationRequested(); - using (MemoryStream stringStream = new(Encoding.UTF8.GetBytes(builtImage.Config))) - { - var configDigest = builtImage.ImageDigest!; - _logger.LogInformation(Strings.Registry_ConfigUploadStarted, configDigest); - await UploadBlobAsync(destination.Repository, configDigest, stringStream, cancellationToken).ConfigureAwait(false); - _logger.LogInformation(Strings.Registry_ConfigUploaded); + cancellationToken.ThrowIfCancellationRequested(); + using (MemoryStream stringStream = new(Encoding.UTF8.GetBytes(builtImage.Config))) + { + var configDigest = builtImage.ImageDigest!; + _logger.LogInformation(Strings.Registry_ConfigUploadStarted, configDigest); + await UploadBlobAsync(destination.Repository, configDigest, stringStream, cancellationToken).ConfigureAwait(false); + _logger.LogInformation(Strings.Registry_ConfigUploaded); + } } // Tags can refer to an image manifest or an image manifest list. @@ -668,7 +682,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, _logger.LogInformation(Strings.Registry_TagUploaded, tag, RegistryName); } } - else + else if (!manifestExists) { _logger.LogInformation(Strings.Registry_ManifestUploadStarted, RegistryName, builtImage.ManifestDigest); await _registryAPI.Manifest.PutAsync(destination.Repository, builtImage.ManifestDigest, builtImage.Manifest, builtImage.ManifestMediaType, cancellationToken).ConfigureAwait(false); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs index c2320bc5174d..2bae6c608622 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs @@ -762,6 +762,15 @@ internal static string Registry_ManifestUploaded { } } + /// + /// Looks up a localized string similar to Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads.. + /// + internal static string Registry_ManifestExists { + get { + return ResourceManager.GetString("Registry_ManifestExists", resourceCulture); + } + } + /// /// Looks up a localized string similar to Uploading manifest to registry '{0}' as blob '{1}'.. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx index 7a9cce993870..0637ecd19f0c 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx @@ -453,6 +453,10 @@ Uploaded manifest to '{0}'. {0} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. {0} is the registry name 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..af67e46b8e8b 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 @@ -399,6 +399,11 @@ Nahrávání vrstvy {0} do {1} bylo dokončeno. {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Manifest se nahrává do registru {0} jako objekt blob {1}. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..fd601564236a 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 @@ -399,6 +399,11 @@ Das Hochladen der Ebene „{0}“ nach „{1}“ wurde abgeschlossen. {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Das Manifest wird in die Registrierung „{0}“ als Blob „{1}“ hochgeladen. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..bed53ed7c8fa 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 @@ -399,6 +399,11 @@ Finalizó la carga de la capa "{0}" en "{1}". {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Cargando manifiesto en el Registro "{0}" como blob "{1}". @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..f293367e8a47 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 @@ -399,6 +399,11 @@ Fin du chargement de la couche «{0}» vers «{1}». {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Chargement du manifeste dans le Registre '{0}' en tant qu’objet blob '{1}'. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..6df14ca3cb17 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 @@ -399,6 +399,11 @@ Caricamento del livello '{0}' in '{1}' completato. {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Caricamento del manifesto nel Registro di sistema '{0}' come BLOB '{1}'. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..79c947dc81df 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 @@ -399,6 +399,11 @@ レイヤー '{0}' の '{1}' へのアップロードが完了しました。 {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. BLOB '{0}' としてレジストリ '{1}' にマニフェストをアップロードしています。 @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..68a801b63feb 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 @@ -399,6 +399,11 @@ '{1}'에 '{0}' 계층을 업로드했습니다. {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. '{0}' 레지스트리에 매니페스트를 '{1}' Blob으로 업로드하는 중입니다. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..8bcf7aa61b58 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 @@ -399,6 +399,11 @@ Zakończono przekazywanie warstwy „{0}” do rejestru „{1}”. {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Przekazywanie manifestu do rejestru „{0}” jako obiektu blob „{1}”. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..0f63c0fb8559 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 @@ -399,6 +399,11 @@ Camada de carregamento concluída '{0}' para '{1}'. {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Carregamento do manifesto para o registro '{0}' como blob '{1}'. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..24ec12d180ef 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 @@ -399,6 +399,11 @@ Завершена отправка слоя "{0}" в "{1}". {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Отправка манифеста в реестр "{0}" как BLOB-объект "{1}". @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..aecd13fd48fe 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 @@ -399,6 +399,11 @@ '{0}' katmanının, '{1}' kayıt defterine karşıya yüklenmesi tamamlandı. {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. Bildirim, '{0}' kayıt defterine '{1}' blob olarak karşıya yükleniyor. @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..c11c391d7cdd 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 @@ -399,6 +399,11 @@ 已完成将层“{0}”上传到“{1}”。 {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. 正在将清单作为 blob“{1}”上传到注册表“{0}”。 @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..7d15e4b79b4c 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 @@ -399,6 +399,11 @@ 已完成上傳圖層 '{0}' 至 '{1}'。 {0} is the layer digest, {1} is the registry name + + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + Manifest '{0}' already exists in repository '{1}'. Skipping layer and configuration uploads. + {0} is the manifest digest, {1} is the repository name + Uploading manifest to registry '{0}' as blob '{1}'. 上傳資訊清單至登錄 '{0}' 做為 blob '{1}'。 @@ -491,4 +496,4 @@ - \ No newline at end of file + 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..29eeacbe5f49 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -166,6 +166,11 @@ partial class CreateNewImage /// public bool SkipPublishing { get; set; } + /// + /// If true, the tooling will upload the image without checking whether its manifest already exists in the destination registry. + /// + public bool NoCache { get; set; } + [Output] public string GeneratedContainerManifest { get; set; } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index 64506e88b352..109fd9480393 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -227,7 +227,7 @@ private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuild if (!SkipPublishing) { - await ImagePublisher.PublishImageAsync(builtImage, sourceImageReference, destinationImageReference, Log, telemetry, cancellationToken) + await ImagePublisher.PublishImageAsync(builtImage, sourceImageReference, destinationImageReference, NoCache, Log, telemetry, cancellationToken) .ConfigureAwait(false); } diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets index 07f09c6aa9b8..85bc0f746fe4 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets @@ -100,6 +100,7 @@ latest $([System.DateTime]::UtcNow.ToString('yyyyMMddhhmmss')) + false @@ -333,6 +335,7 @@ _ContainerEnvironmentVariables=@(ContainerEnvironmentVariable->'%(Identity):%(Value)'); ContainerGenerateLabels=$(ContainerGenerateLabels); ContainerGenerateLabelsImageBaseDigest=$(ContainerGenerateLabelsImageBaseDigest); + ContainerPushNoCache=$(ContainerPushNoCache); _SkipContainerPublishing=$(_SkipContainerPublishing); ContainerImageFormat=$(_SingleImageContainerFormat); _IsMultiRIDBuild=false; diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs index 6eb58542ad46..340a2e08ed20 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -1628,8 +1628,8 @@ public async Task CheckDownloadErrorMessageWhenSourceRepositoryThrows() (var taskLog, var errors) = SetupTaskLog(); var telemetry = new Telemetry(sourceReference, destinationReference, taskLog); - await ImagePublisher.PublishImageAsync(builtImage, sourceReference, destinationReference, taskLog, telemetry, CancellationToken.None) - .ConfigureAwait(false); + await ImagePublisher.PublishImageAsync(builtImage, sourceReference, destinationReference, false, taskLog, telemetry, CancellationToken.None) + .ConfigureAwait(false); // Assert the error message Assert.IsTrue(taskLog.HasLoggedErrors); diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs b/test/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs index ed6535a9b64b..f945ca5ac5ca 100644 --- a/test/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs +++ b/test/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs @@ -52,6 +52,130 @@ public void DockerIoAlias() Assert.AreEqual("registry-1.docker.io", registry.BaseUri.Host); } + [DataRow(HttpStatusCode.OK, true)] + [DataRow(HttpStatusCode.NotFound, false)] + [DataRow(HttpStatusCode.Unauthorized, false)] + [DataRow(HttpStatusCode.Forbidden, false)] + [DataRow(HttpStatusCode.MethodNotAllowed, false)] + [TestMethod] + public async Task ManifestExistsAsync_ReturnsExpectedResult(HttpStatusCode statusCode, bool expected) + { + ILogger logger = _loggerFactory.CreateLogger(nameof(ManifestExistsAsync_ReturnsExpectedResult)); + Mock client = new(MockBehavior.Loose); + HttpRequestMessage? request = null; + client.Setup(c => c.SendAsync(It.IsAny(), It.IsAny())) + .Callback((message, _) => request = message) + .ReturnsAsync(new HttpResponseMessage(statusCode)); + DefaultManifestOperations operations = new(new Uri("https://example.com"), "example.com", client.Object, logger); + + bool actual = await operations.ExistsAsync("test/repository", "sha256:1234", CancellationToken.None); + + Assert.AreEqual(expected, actual); + Assert.IsNotNull(request); + Assert.AreEqual(HttpMethod.Head, request.Method); + Assert.AreEqual("https://example.com/v2/test/repository/manifests/sha256:1234", request.RequestUri!.AbsoluteUri); + Assert.IsNotEmpty(request.Headers.Accept); + } + + [TestMethod] + public async Task ManifestExistsAsync_PropagatesServerErrors() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(ManifestExistsAsync_PropagatesServerErrors)); + Mock client = new(MockBehavior.Loose); + client.Setup(c => c.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + DefaultManifestOperations operations = new(new Uri("https://example.com"), "example.com", client.Object, logger); + + await Assert.ThrowsAsync(() => operations.ExistsAsync("test/repository", "sha256:1234", CancellationToken.None)); + } + + [TestMethod] + public async Task PushAsync_SkipsBlobUploadsByDefaultWhenManifestAlreadyExists() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(PushAsync_SkipsBlobUploadsByDefaultWhenManifestAlreadyExists)); + const string repository = "test/repository"; + const string manifestDigest = "sha256:manifest"; + string[] tags = ["latest", "stable"]; + + Mock manifestOperations = new(MockBehavior.Strict); + manifestOperations + .Setup(m => m.ExistsAsync(repository, manifestDigest, It.IsAny())) + .ReturnsAsync(true); + foreach (string tag in tags) + { + manifestOperations + .Setup(m => m.PutAsync(repository, tag, "{}", SchemaTypes.OciManifestV1, It.IsAny())) + .Returns(Task.CompletedTask); + } + + Mock api = new(MockBehavior.Strict); + api.SetupGet(a => a.Manifest).Returns(manifestOperations.Object); + Registry registry = new("example.com", logger, api.Object); + BuiltImage image = new() + { + Config = "{}", + ImageDigest = "sha256:config", + Manifest = "{}", + ManifestDigest = manifestDigest, + ManifestMediaType = SchemaTypes.OciManifestV1, + Layers = [new ManifestLayer(SchemaTypes.OciLayerGzipV1, 123, "sha256:layer", null)], + OS = "linux", + Architecture = "amd64", + }; + SourceImageReference source = new(registry, "base/image", "latest", null); + DestinationImageReference destination = new(registry, repository, tags); + + await registry.PushAsync(image, source, destination, CancellationToken.None); + + manifestOperations.Verify(m => m.ExistsAsync(repository, manifestDigest, It.IsAny()), Times.Once); + foreach (string tag in tags) + { + manifestOperations.Verify(m => m.PutAsync(repository, tag, "{}", SchemaTypes.OciManifestV1, It.IsAny()), Times.Once); + } + api.VerifyGet(a => a.Blob, Times.Never); + } + + [TestMethod] + public async Task PushAsync_DoesNotCheckManifestWhenNoCacheIsEnabled() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(PushAsync_DoesNotCheckManifestWhenNoCacheIsEnabled)); + const string repository = "test/repository"; + const string configDigest = "sha256:config"; + + Mock blobOperations = new(MockBehavior.Strict); + blobOperations + .Setup(b => b.ExistsAsync(repository, configDigest, It.IsAny())) + .ReturnsAsync(true); + Mock manifestOperations = new(MockBehavior.Strict); + manifestOperations + .Setup(m => m.PutAsync(repository, "latest", "{}", SchemaTypes.OciManifestV1, It.IsAny())) + .Returns(Task.CompletedTask); + Mock api = new(MockBehavior.Strict); + api.SetupGet(a => a.Blob).Returns(blobOperations.Object); + api.SetupGet(a => a.Manifest).Returns(manifestOperations.Object); + + Registry registry = new("example.com", logger, api.Object); + BuiltImage image = new() + { + Config = "{}", + ImageDigest = configDigest, + Manifest = "{}", + ManifestDigest = "sha256:manifest", + ManifestMediaType = SchemaTypes.OciManifestV1, + Layers = [], + OS = "linux", + Architecture = "amd64", + }; + SourceImageReference source = new(registry, "base/image", "latest", null); + DestinationImageReference destination = new(registry, repository, ["latest"]); + + await registry.PushAsync(image, source, destination, noCache: true, CancellationToken.None); + + manifestOperations.Verify(m => m.ExistsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + blobOperations.Verify(b => b.ExistsAsync(repository, configDigest, It.IsAny()), Times.Once); + manifestOperations.Verify(m => m.PutAsync(repository, "latest", "{}", SchemaTypes.OciManifestV1, It.IsAny()), Times.Once); + } + [TestMethod] public async Task RegistriesThatProvideNoUploadSizeAttemptFullUpload() {