diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs index 665869e4a7a0..3885dd728fcd 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -215,6 +215,11 @@ private static async Task PushToLocalRegistryAsync(ILogger logger, BuiltIma await containerRegistry.LoadAsync(builtImage, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false); logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToLocalDaemon, destinationImageReference, containerRegistry); } + catch (UnableToDownloadFromRepositoryException) + { + logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference); + return 1; + } catch (Exception ex) { logger.LogError(Resource.FormatString(nameof(Strings.RegistryOutputPushFailed), ex.Message)); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Exceptions/UnableToDownloadFromRepositoryException.cs b/src/Containers/Microsoft.NET.Build.Containers/Exceptions/UnableToDownloadFromRepositoryException.cs new file mode 100644 index 000000000000..c7fc70b6cd93 --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/Exceptions/UnableToDownloadFromRepositoryException.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.NET.Build.Containers; + +internal sealed class UnableToDownloadFromRepositoryException : Exception +{ + public UnableToDownloadFromRepositoryException(string repository) + : base($"The download of the image from repository { repository } has failed.") + { + } +} \ No newline at end of file diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs b/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs index 1a16cbbad500..d6a9c56850ec 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs @@ -107,6 +107,10 @@ private static async Task PushToLocalRegistryAsync( await loadFunc(image, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false); Log.LogMessage(MessageImportance.High, Strings.ContainerBuilder_ImageUploadedToLocalDaemon, destinationImageReference, localRegistry); } + catch (UnableToDownloadFromRepositoryException) + { + Log.LogErrorWithCodeFromResources(nameof(Strings.UnableToDownloadFromRepository), sourceImageReference); + } catch (ContainerHttpException e) { Log.LogErrorFromException(e, true); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs index 1b01ebd5bdc4..2deb1de662a6 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs @@ -74,6 +74,8 @@ internal sealed class Registry private const string DockerHubRegistry1 = "registry-1.docker.io"; private const string DockerHubRegistry2 = "registry.hub.docker.com"; private static readonly int s_defaultChunkSizeBytes = 1024 * 64; + private const int MaxDownloadRetries = 5; + private readonly Func _retryDelayProvider; private readonly ILogger _logger; private readonly IRegistryAPI _registryAPI; @@ -86,7 +88,7 @@ internal sealed class Registry /// public string RegistryName { get; } - internal Registry(string registryName, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null) : + internal Registry(string registryName, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null, Func? retryDelayProvider = null) : this(new Uri($"https://{registryName}"), logger, registryAPI, settings) { } @@ -95,7 +97,7 @@ internal Registry(string registryName, ILogger logger, RegistryMode mode, Regist { } - internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null) : + internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null, Func? retryDelayProvider = null) : this(baseUri, logger, new RegistryApiFactory(registryAPI), settings) { } @@ -103,7 +105,7 @@ internal Registry(Uri baseUri, ILogger logger, RegistryMode mode, RegistrySettin this(baseUri, logger, new RegistryApiFactory(mode), settings) { } - private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, RegistrySettings? settings = null) + private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, RegistrySettings? settings = null, Func? retryDelayProvider = null) { RegistryName = DeriveRegistryName(baseUri); @@ -117,6 +119,8 @@ private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, Regist _logger = logger; _settings = settings ?? new RegistrySettings(RegistryName); _registryAPI = factory.Create(RegistryName, BaseUri, logger, _settings.IsInsecure); + + _retryDelayProvider = retryDelayProvider ?? (() => TimeSpan.FromSeconds(1)); } private static string DeriveRegistryName(Uri baseUri) @@ -401,26 +405,48 @@ public async Task DownloadBlobAsync(string repository, Descriptor descri { cancellationToken.ThrowIfCancellationRequested(); string localPath = ContentStore.PathForDescriptor(descriptor); - + if (File.Exists(localPath)) { // Assume file is up to date and just return it return localPath; } - - // No local copy, so download one - using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false); - + string tempTarballPath = ContentStore.GetTempFile(); - using (FileStream fs = File.Create(tempTarballPath)) + + int retryCount = 0; + while (retryCount < MaxDownloadRetries) { - await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); + try + { + // No local copy, so download one + using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false); + + using (FileStream fs = File.Create(tempTarballPath)) + { + await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); + } + + // Break the loop if successful + break; + } + catch (Exception ex) + { + retryCount++; + if (retryCount >= MaxDownloadRetries) + { + throw new UnableToDownloadFromRepositoryException(repository); + } + + _logger.LogTrace("Download attempt {0}/{1} for repository '{2}' failed. Error: {3}", retryCount, MaxDownloadRetries, repository, ex.ToString()); + + // Wait before retrying + await Task.Delay(_retryDelayProvider(), cancellationToken).ConfigureAwait(false); + } } - - cancellationToken.ThrowIfCancellationRequested(); - + File.Move(tempTarballPath, localPath, overwrite: true); - + return localPath; } 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 ec4966caaa27..6aeedcd6f527 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs @@ -844,6 +844,15 @@ internal static string UnableToAccessRepository { } } + /// + /// Looks up a localized string similar to CONTAINER1018: Unable to download image from the repository '{0}'.. + /// + internal static string UnableToDownloadFromRepository { + get { + return ResourceManager.GetString("UnableToDownloadFromRepository", resourceCulture); + } + } + /// /// Looks up a localized string similar to CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}.. /// diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx index b0e7350da710..747ba4d5fdaf 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx @@ -461,6 +461,10 @@ CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. {StrBegin="CONTAINER1015: "} + + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. {StrBegin="CONTAINER1016:" } 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 383c7346ddca..9c3e2c722272 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 @@ -429,6 +429,11 @@ CONTAINER1016: Nelze získat přístup k úložišti „{0}“ v registru „{1}“. Ověřte prosím správnost vašich přihlašovacích údajů a to, že máte přístup k tomuto úložišti a registru. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: Neznámé AppCommandInstruction „{0}“. Platné pokyny jsou {1}. 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 f273669dca6c..22bba4833144 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 @@ -429,6 +429,11 @@ CONTAINER1016: Auf das Repository "{0}" in der Registrierung "{1}" kann nicht zugegriffen werden. Vergewissern Sie sich, dass Ihre Anmeldeinformationen korrekt sind und dass Sie Zugriff auf dieses Repository und die Registrierung haben. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: Unbekannte AppCommandInstruction "{0}". Gültige Anweisungen sind {1}. 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 fb785d4e4f7d..10471e388cea 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 @@ -429,6 +429,11 @@ CONTAINER1016: no se puede acceder al repositorio ''{0}'' en el registro ''{1}''. Confirme que las credenciales son correctas y que tiene acceso a este repositorio y registro. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: AppCommandInstruction ''{0}desconocido. Las instrucciones válidas son {1}. 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 ff593f5316cb..4412128004e1 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 @@ -429,6 +429,11 @@ CONTAINER1016: nous n’avons pas pu accéder au référentiel '{0}' dans le Registre '{1}'. Confirmez que vos informations d’identification sont correctes et que vous avez accès à ce référentiel et à ce Registre. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: instruction de commande d'application inconnue '{0}'. Les instructions valides sont {1}. 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 0a8f8a977150..c397a460e871 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 @@ -429,6 +429,11 @@ CONTAINER1016: impossibile accedere al repository '{0}' nel Registro di sistema '{1}'. Verificare che le credenziali siano corrette e di avere accesso a questo repository e registro. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: appCommandInstruction '{0}'sconosciuta. Istruzioni valide sono {1}. 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 6cf8d131368c..3000a8222b31 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 @@ -429,6 +429,11 @@ CONTAINER1016: レジストリ '{1}' のリポジトリ '{0}' にアクセスできません。資格情報が正しいこと、およびこのリポジトリとレジストリへのアクセス権があることを確認してください。 {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: 不明な AppCommandInstruction '{0}'。有効な手順は {1} です。 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 77de4d142dd3..d78df1774f87 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 @@ -429,6 +429,11 @@ CONTAINER1016: '{1}' 레지스트리의 '{0}' 리포지토리에 액세스할 수 없습니다. 자격 증명이 올바르고 이 리포지토리 및 레지스트리에 액세스할 수 있는지 확인하세요. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: 알 수 없는 AppCommandInstruction '{0}'. 올바른 지침은 {1}입니다. 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 6eeb96f72d6d..a982c84867bd 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 @@ -429,6 +429,11 @@ CONTAINER1016: nie można uzyskać dostępu do repozytorium „{0}” w rejestrze „{1}”. Upewnij się, że poświadczenia są poprawne oraz że masz dostęp do tego repozytorium i rejestru. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: Nieznana instrukcja AppCommandInstruction „{0}”. Prawidłowe instrukcje to{1}. 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 9686e1444a08..6fab102385c0 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 @@ -429,6 +429,11 @@ CONTAINER1016: não é possível acessar o repositório ''{0}'' no registro ''{1}''. Confirme se suas credenciais estão corretas e se você tem acesso a este repositório e ao Registro. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: AppCommandInstruction desconhecido '{0}'. As instruções válidas são {1}. 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 8b942db5bfef..2ffb60aa8c50 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 @@ -429,6 +429,11 @@ CONTAINER1016: не удается получить доступ к репозиторию "{0}" в реестре "{1}". Убедитесь, что ваши учетные данные верны и что у вас есть доступ к этому репозиторию и реестру. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: неизвестный элемент AppCommandInstruction "{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 146196fba1bc..cb447d8be331 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 @@ -429,6 +429,11 @@ CONTAINER1016: '{1}' kayıt defterindeki '{0}' deposuna erişilemedi. Lütfen kimlik bilgilerinizin doğru olduğunu ve bu depoya ve kayıt defterine erişiminiz olduğunu onaylayın. {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: AppCommandInstruction '{0}' bilinmiyor. Geçerli yönergeler {1}. 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 ab3a5f1b5993..4e5ad069e8bc 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 @@ -429,6 +429,11 @@ CONTAINER1016: 无法访问注册表“{1}”中的存储库“{0}”。请确认你的凭据正确无误,并且你有权访问此存储库和注册表。 {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: AppCommandInstruction“{0}”未知。有效的说明为 {1}。 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 9a144ff7f80b..f8ed2e82175b 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 @@ -429,6 +429,11 @@ CONTAINER1016: 無法存取登錄 '{1}' 中的存放庫 '{0}'。請確認您的認證正確,且您擁有此存放庫和登錄的存取權。 {StrBegin="CONTAINER1016:" } + + CONTAINER1018: Unable to download image from the repository '{0}'. + CONTAINER1018: Unable to download image from the repository '{0}'. + {StrBegins="CONTAINER1018:" } + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. CONTAINER2021: 未知的 AppCommandInstruction '{0}'。有效的指示為 {1}。 diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs index c630ef45e541..a9b228a8ba14 100644 --- a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -4,6 +4,8 @@ using System.Formats.Tar; using System.Runtime.CompilerServices; using System.Text.Json; +using FakeItEasy; +using Microsoft.Build.Framework; using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Build.Containers.LocalDaemons; using Microsoft.NET.Build.Containers.Resources; @@ -1428,6 +1430,71 @@ static string[] DecideEntrypoint(string rid, string appName, string workingDir) } } + [DockerAvailableFact] + public async void CheckDownloadErrorMessageWhenSourceRepositoryThrows() + { + var loggerFactory = new TestLoggerFactory(_testOutput); + var logger = loggerFactory.CreateLogger(nameof(CheckDownloadErrorMessageWhenSourceRepositoryThrows)); + string rid = "win-x64"; + string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework, rid: rid); + + // Build the image + Registry registry = new(DockerRegistryManager.BaseImageSource, logger, RegistryMode.Push); + ImageBuilder? imageBuilder = await registry.GetImageManifestAsync( + DockerRegistryManager.RuntimeBaseImage, + DockerRegistryManager.Net8PreviewWindowsSpecificImageTag, + rid, + ToolsetUtils.RidGraphManifestPicker, + cancellationToken: default).ConfigureAwait(false); + Assert.NotNull(imageBuilder); + + Layer l = Layer.FromDirectory(publishDirectory, "C:\\app", true, imageBuilder.ManifestMediaType); + + imageBuilder.AddLayer(l); + imageBuilder.SetWorkingDirectory("C:\\app"); + + string[] entryPoint = DecideEntrypoint(rid, "MinimalTestApp", "C:\\app"); + imageBuilder.SetEntrypointAndCmd(entryPoint, Array.Empty()); + + BuiltImage builtImage = imageBuilder.Build(); + + // Load the image into the local registry + var sourceReference = new SourceImageReference(registry, "some_random_image", DockerRegistryManager.Net8ImageTag); + string archivePath = Path.Combine(TestSettings.TestArtifactsDirectory, nameof(CheckDownloadErrorMessageWhenSourceRepositoryThrows)); + var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archivePath), NewImageName(), new[] { rid }); + + (var taskLog, var errors) = SetupTaskLog(); + var telemetry = new Telemetry(sourceReference, destinationReference, taskLog); + + await ImagePublisher.PublishImageAsync(builtImage, sourceReference, destinationReference, taskLog, telemetry, CancellationToken.None) + .ConfigureAwait(false); + + // Assert the error message + Assert.True(taskLog.HasLoggedErrors); + Assert.NotNull(errors); + Assert.Single(errors); + Assert.Contains("Unable to download image from the repository", errors[0]); + + static string[] DecideEntrypoint(string rid, string appName, string workingDir) + { + var binary = rid.StartsWith("win", StringComparison.Ordinal) ? $"{appName}.exe" : appName; + return new[] { $"{workingDir}/{binary}" }; + } + + static (Microsoft.Build.Utilities.TaskLoggingHelper, List errors) SetupTaskLog() + { + // We can use any Task, we just need TaskLoggingHelper + Tasks.CreateNewImage cni = new(); + List errors = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => errors.Add(e.Message)); + A.CallTo(() => buildEngine.LogErrorEvent(A.Ignored)).Invokes((BuildErrorEventArgs e) => errors.Add(e.Message)); + A.CallTo(() => buildEngine.LogMessageEvent(A.Ignored)).Invokes((BuildMessageEventArgs e) => errors.Add(e.Message)); + cni.BuildEngine = buildEngine; + return (cni.Log, errors); + } + } + [DockerAvailableFact(checkContainerdStoreAvailability: true)] public void EnforcesOciSchemaForMultiRIDTarballOutput() { diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs index 51fad494918f..8cb8cf07e2c1 100644 --- a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs @@ -539,6 +539,77 @@ public void IsRegistryInsecure(string registryName, string? insecureRegistriesEn Assert.Equal(expectedInsecure, registrySettings.IsInsecure); } + [Fact] + public async Task DownloadBlobAsync_RetriesOnFailure() + { + // Arrange + var logger = _loggerFactory.CreateLogger(nameof(DownloadBlobAsync_RetriesOnFailure)); + + var repoName = "testRepo"; + var descriptor = new Descriptor(SchemaTypes.OciLayerGzipV1, "sha256:testdigest1234", 1234); + var cancellationToken = CancellationToken.None; + + var mockRegistryAPI = new Mock(MockBehavior.Strict); + mockRegistryAPI + .SetupSequence(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken)) + .ThrowsAsync(new Exception("Simulated failure 1")) // First attempt fails + .ThrowsAsync(new Exception("Simulated failure 2")) // Second attempt fails + .ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })); // Third attempt succeeds + + Registry registry = new(repoName, logger, mockRegistryAPI.Object, null, () => TimeSpan.Zero); + + string? result = null; + try + { + // Act + result = await registry.DownloadBlobAsync(repoName, descriptor, cancellationToken); + + // Assert + Assert.NotNull(result); + Assert.True(File.Exists(result)); // Ensure the file was successfully downloaded + mockRegistryAPI.Verify(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken), Times.Exactly(3)); // Verify retries + } + finally + { + // Cleanup + if (result != null) + { + File.Delete(result); + } + } + } + + [Fact] + public async Task DownloadBlobAsync_ThrowsAfterMaxRetries() + { + // Arrange + var logger = _loggerFactory.CreateLogger(nameof(DownloadBlobAsync_ThrowsAfterMaxRetries)); + + var repoName = "testRepo"; + var descriptor = new Descriptor(SchemaTypes.OciLayerGzipV1, "sha256:testdigest1234", 1234); + var cancellationToken = CancellationToken.None; + + var mockRegistryAPI = new Mock(MockBehavior.Strict); + // Simulate 5 failures (assuming your retry logic attempts 5 times before throwing) + mockRegistryAPI + .SetupSequence(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken)) + .ThrowsAsync(new Exception("Simulated failure 1")) + .ThrowsAsync(new Exception("Simulated failure 2")) + .ThrowsAsync(new Exception("Simulated failure 3")) + .ThrowsAsync(new Exception("Simulated failure 4")) + .ThrowsAsync(new Exception("Simulated failure 5")); + + Registry registry = new(repoName, logger, mockRegistryAPI.Object, null, () => TimeSpan.Zero); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await registry.DownloadBlobAsync(repoName, descriptor, cancellationToken); + }); + + mockRegistryAPI.Verify(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken), Times.Exactly(5)); + } + private static NextChunkUploadInformation ChunkUploadSuccessful(Uri requestUri, Uri uploadUrl, int? contentLength, HttpStatusCode code = HttpStatusCode.Accepted) { return new(uploadUrl);