From d0917b8130eb2f6b1ba46983aa2dae7a23a592d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Sat, 26 Oct 2024 11:44:02 +0000 Subject: [PATCH 1/4] Add support for using Digest for base image Add support for using image digest (sha256) for the base image when using the `PublishContainer` target. For example: `mcr.microsoft.com/dotnet/aspnet@sha256:6cec36412a215aad2a033cfe259890482be0a1dcb680e81fccc393b2d4069455` --- .../ContainerBuilder.cs | 8 ++++-- .../KnownStrings.cs | 1 + .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 5 +++- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 5 +++- .../SourceImageReference.cs | 19 ++++++++----- .../Tasks/CreateNewImage.Interface.cs | 10 +++++-- .../Tasks/CreateNewImage.cs | 7 +++-- .../Tasks/CreateNewImageToolTask.cs | 4 +++ .../Tasks/ParseContainerProperties.cs | 8 +++++- .../containerize/ContainerizeCommand.cs | 9 ++++++ .../Microsoft.NET.Build.Containers.targets | 2 ++ .../CreateNewImageTests.cs | 2 +- .../DockerRegistryManager.cs | 6 ++-- .../DockerRegistryTests.cs | 2 +- .../EndToEndTests.cs | 18 ++++++++---- .../ContainerHelpersTests.cs | 28 +++++++++++-------- 16 files changed, 94 insertions(+), 40 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs index 48867a93de6b..a89bf37b3321 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -14,6 +14,7 @@ internal static async Task ContainerizeAsync( string baseRegistry, string baseImageName, string baseImageTag, + string? baseImageDigest, string[] entrypoint, string[] entrypointArgs, string[] defaultArgs, @@ -47,7 +48,7 @@ internal static async Task ContainerizeAsync( bool isLocalPull = string.IsNullOrEmpty(baseRegistry); RegistryMode sourceRegistryMode = baseRegistry.Equals(outputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull; Registry? sourceRegistry = isLocalPull ? null : new Registry(baseRegistry, logger, sourceRegistryMode); - SourceImageReference sourceImageReference = new(sourceRegistry, baseImageName, baseImageTag); + SourceImageReference sourceImageReference = new(sourceRegistry, baseImageName, baseImageTag, baseImageDigest); DestinationImageReference destinationImageReference = DestinationImageReference.CreateFromSettings( imageName, @@ -62,17 +63,18 @@ internal static async Task ContainerizeAsync( { try { + string imageReference = !string.IsNullOrEmpty(baseImageDigest) ? baseImageDigest : baseImageTag; var ridGraphPicker = new RidGraphManifestPicker(ridGraphPath); imageBuilder = await registry.GetImageManifestAsync( baseImageName, - baseImageTag, + imageReference, containerRuntimeIdentifier, ridGraphPicker, cancellationToken).ConfigureAwait(false); } catch (RepositoryNotFoundException) { - logger.LogError(Resource.FormatString(nameof(Strings.RepositoryNotFound), baseImageName, baseImageTag, registry.RegistryName)); + logger.LogError(Resource.FormatString(nameof(Strings.RepositoryNotFound), baseImageName, baseImageTag, baseImageDigest, registry.RegistryName)); return 1; } catch (UnableToAccessRepositoryException) diff --git a/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs b/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs index 371edab873a6..7a6ddc08c7c4 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs @@ -31,6 +31,7 @@ public static class Properties public static readonly string ContainerBaseRegistry = nameof(ContainerBaseRegistry); public static readonly string ContainerBaseName = nameof(ContainerBaseName); public static readonly string ContainerBaseTag = nameof(ContainerBaseTag); + public static readonly string ContainerBaseDigest = nameof(ContainerBaseDigest); public static readonly string ContainerGenerateLabels = nameof(ContainerGenerateLabels); diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net10.0/PublicAPI.Unshipped.txt index b19e4fa81718..f11ef4cc85f9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -176,6 +176,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageDigest.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageDigest.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Cancel() -> void @@ -262,6 +264,7 @@ Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParseContainerProp Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerImage.get -> string! Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerTag.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerDigest.get -> string! override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Execute() -> bool override Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.Execute() -> bool static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string! input, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool @@ -321,4 +324,4 @@ static Microsoft.NET.Build.Containers.ImageIndexV1.operator ==(Microsoft.NET.Bui override Microsoft.NET.Build.Containers.ImageIndexV1.GetHashCode() -> int ~override Microsoft.NET.Build.Containers.ImageIndexV1.Equals(object obj) -> bool Microsoft.NET.Build.Containers.ImageIndexV1.Equals(Microsoft.NET.Build.Containers.ImageIndexV1 other) -> bool -Microsoft.NET.Build.Containers.ImageIndexV1.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificOciManifest[]! manifests) -> void \ No newline at end of file +Microsoft.NET.Build.Containers.ImageIndexV1.Deconstruct(out int schemaVersion, out string! mediaType, out Microsoft.NET.Build.Containers.PlatformSpecificOciManifest[]! manifests) -> void diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt index 43526e9c3ab4..5289498137e7 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -29,6 +29,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageDigest.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageDigest.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerEnvironmentVariables.get -> Microsoft.Build.Framework.ITaskItem![]! @@ -113,6 +115,7 @@ Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParseContainerProp Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerImage.get -> string! Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerTag.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerDigest.get -> string! override Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.Execute() -> bool static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string! input, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string? portNumber, string? portType, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool @@ -136,4 +139,4 @@ Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.get Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.set -> void Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetFrameworkVersion.get -> string! Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetFrameworkVersion.set -> void -override Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.Execute() -> bool \ No newline at end of file +override Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.Execute() -> bool diff --git a/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs b/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs index 0775f853dad1..8a37f9b5ca8e 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs @@ -4,20 +4,25 @@ namespace Microsoft.NET.Build.Containers; /// -/// Represents a reference to a Docker image. A reference is made of a registry, a repository (aka the image name) and a tag. +/// Represents a reference to a Docker image. A reference is made of a registry, a repository (aka the image name) and a tag or digest. /// -internal readonly record struct SourceImageReference(Registry? Registry, string Repository, string Tag) +internal readonly record struct SourceImageReference(Registry? Registry, string Repository, string Tag, string? Digest) { public override string ToString() { - if (Registry is { } reg) + string sourceImageReference = RepositoryAndTag; + + if (Registry is { } reg) { - return $"{reg.RegistryName}/{Repository}:{Tag}"; - } - else + sourceImageReference = $"{reg.RegistryName}/{sourceImageReference}"; + } + + if (!string.IsNullOrEmpty(Digest)) { - return RepositoryAndTag; + sourceImageReference = $"{sourceImageReference}@{Digest}"; } + + return sourceImageReference; } /// 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 3da5135b9b07..79dfa4961f6b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -34,9 +34,14 @@ partial class CreateNewImage /// The base image tag. /// Ex: 6.0 /// - [Required] public string BaseImageTag { get; set; } + /// + /// The base image digest. + /// Ex: sha256:12345... + /// + public string BaseImageDigest { get; set; } + /// /// The registry to push to. /// @@ -186,7 +191,8 @@ public CreateNewImage() ToolPath = ""; BaseRegistry = ""; BaseImageName = ""; - BaseImageTag = ""; + BaseImageTag = "latest"; + BaseImageDigest = ""; OutputRegistry = ""; ArchiveOutputPath = ""; Repository = ""; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index 34c4984ae0bb..1a2a3ad3a036 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -62,7 +62,7 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) RegistryMode sourceRegistryMode = BaseRegistry.Equals(OutputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull; Registry? sourceRegistry = IsLocalPull ? null : new Registry(BaseRegistry, logger, sourceRegistryMode); - SourceImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag); + SourceImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag, BaseImageDigest); DestinationImageReference destinationImageReference = DestinationImageReference.CreateFromSettings( Repository, @@ -79,10 +79,11 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) { try { + string imageReference = !string.IsNullOrEmpty(BaseImageDigest) ? BaseImageDigest : BaseImageTag; var picker = new RidGraphManifestPicker(RuntimeIdentifierGraphPath); imageBuilder = await registry.GetImageManifestAsync( BaseImageName, - BaseImageTag, + imageReference, ContainerRuntimeIdentifier, picker, cancellationToken).ConfigureAwait(false); @@ -90,7 +91,7 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) catch (RepositoryNotFoundException) { telemetry.LogUnknownRepository(); - Log.LogErrorWithCodeFromResources(nameof(Strings.RepositoryNotFound), BaseImageName, BaseImageTag, registry.RegistryName); + Log.LogErrorWithCodeFromResources(nameof(Strings.RepositoryNotFound), BaseImageName, BaseImageTag, BaseImageDigest, registry.RegistryName); return !Log.HasLoggedErrors; } catch (UnableToAccessRepositoryException) diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs index 698cea555c10..c1bc8fc24af8 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs @@ -107,6 +107,10 @@ internal string GenerateCommandLineCommandsInt() { builder.AppendSwitchIfNotNull("--baseimagetag ", BaseImageTag); } + if (!string.IsNullOrWhiteSpace(BaseImageDigest)) + { + builder.AppendSwitchIfNotNull("--baseimagedigest ", BaseImageDigest); + } if (!string.IsNullOrWhiteSpace(OutputRegistry)) { builder.AppendSwitchIfNotNull("--outputregistry ", OutputRegistry); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs index 0efe65781c85..bab45e1c1ce1 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs @@ -48,6 +48,9 @@ public sealed class ParseContainerProperties : Microsoft.Build.Utilities.Task [Output] public string ParsedContainerTag { get; private set; } + [Output] + public string ParsedContainerDigest { get; private set; } + [Output] public string NewContainerRegistry { get; private set; } @@ -71,6 +74,7 @@ public ParseContainerProperties() ParsedContainerRegistry = ""; ParsedContainerImage = ""; ParsedContainerTag = ""; + ParsedContainerDigest = ""; NewContainerRegistry = ""; NewContainerRepository = ""; NewContainerTags = Array.Empty(); @@ -132,7 +136,7 @@ public override bool Execute() out string? outputReg, out string? outputImage, out string? outputTag, - out string? _outputDigest, + out string? outputDigest, out bool isRegistrySpecified)) { Log.LogErrorWithCodeFromResources(nameof(Strings.BaseImageNameParsingFailed), nameof(FullyQualifiedBaseImageName), FullyQualifiedBaseImageName); @@ -163,6 +167,7 @@ public override bool Execute() ParsedContainerRegistry = outputReg ?? ""; ParsedContainerImage = outputImage ?? ""; ParsedContainerTag = outputTag ?? ""; + ParsedContainerDigest = outputDigest ?? ""; NewContainerRegistry = ContainerRegistry; NewContainerTags = validTags; @@ -172,6 +177,7 @@ public override bool Execute() Log.LogMessage(MessageImportance.Low, "Host: {0}", ParsedContainerRegistry); Log.LogMessage(MessageImportance.Low, "Image: {0}", ParsedContainerImage); Log.LogMessage(MessageImportance.Low, "Tag: {0}", ParsedContainerTag); + Log.LogMessage(MessageImportance.Low, "Digest: {0}", ParsedContainerDigest); Log.LogMessage(MessageImportance.Low, "Image Name: {0}", NewContainerRepository); Log.LogMessage(MessageImportance.Low, "Image Tags: {0}", string.Join(", ", NewContainerTags)); } diff --git a/src/Containers/containerize/ContainerizeCommand.cs b/src/Containers/containerize/ContainerizeCommand.cs index cba5b0a012b7..fd47e728142c 100644 --- a/src/Containers/containerize/ContainerizeCommand.cs +++ b/src/Containers/containerize/ContainerizeCommand.cs @@ -35,6 +35,12 @@ internal class ContainerizeCommand : CliRootCommand DefaultValueFactory = (_) => "latest" }; + internal CliOption BaseImageDigestOption { get; } = new("--baseimagedigest") + { + Description = "The base image digest. Ex: sha256:6cec3641...", + Required = false + }; + internal CliOption OutputRegistryOption { get; } = new("--outputregistry") { Description = "The registry to push to.", @@ -204,6 +210,7 @@ internal ContainerizeCommand() : base("Containerize an application without Docke Options.Add(BaseRegistryOption); Options.Add(BaseImageNameOption); Options.Add(BaseImageTagOption); + Options.Add(BaseImageDigestOption); Options.Add(OutputRegistryOption); Options.Add(ArchiveOutputPathOption); Options.Add(RepositoryOption); @@ -232,6 +239,7 @@ internal ContainerizeCommand() : base("Containerize an application without Docke string _baseReg = parseResult.GetValue(BaseRegistryOption)!; string _baseName = parseResult.GetValue(BaseImageNameOption)!; string _baseTag = parseResult.GetValue(BaseImageTagOption)!; + string? _baseDigest = parseResult.GetValue(BaseImageDigestOption); string? _outputReg = parseResult.GetValue(OutputRegistryOption); string? _archiveOutputPath = parseResult.GetValue(ArchiveOutputPathOption); string _name = parseResult.GetValue(RepositoryOption)!; @@ -264,6 +272,7 @@ await ContainerBuilder.ContainerizeAsync( _baseReg, _baseName, _baseTag, + _baseDigest, _entrypoint, _entrypointArgs, _defaultArgs, diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets index e9e52d5dfece..50458adc2a37 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets @@ -130,6 +130,7 @@ + @@ -254,6 +255,7 @@ BaseRegistry="$(ContainerBaseRegistry)" BaseImageName="$(ContainerBaseName)" BaseImageTag="$(ContainerBaseTag)" + BaseImageDigest="$(ContainerBaseDigest)" LocalRegistry="$(LocalRegistry)" OutputRegistry="$(ContainerRegistry)" ArchiveOutputPath="$(ContainerArchiveOutputPath)" diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs index 0f5ae607e328..e1a8055cd158 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs @@ -242,7 +242,7 @@ public async System.Threading.Tasks.Task CreateNewImage_RootlessBaseImage() BuiltImage builtImage = imageBuilder.Build(); - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net8ImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net8ImageTag, null); var destinationReference = new DestinationImageReference(registry, RootlessBase, new[] { "latest" }); await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false); diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs index 7c460b5aad66..65e5f09b4cb6 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs @@ -16,10 +16,12 @@ public class DockerRegistryManager public const string Net7ImageTag = "7.0"; public const string Net8ImageTag = "8.0"; public const string Net9ImageTag = "9.0"; + public const string Net9ImageDigest = "sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345"; public const string Net8PreviewWindowsSpecificImageTag = $"{Net8ImageTag}-nanoserver-ltsc2022"; public const string LocalRegistry = "localhost:5010"; public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}/{RuntimeBaseImage}:{Net9ImageTag}"; public const string FullyQualifiedBaseImageAspNet = $"{BaseImageSource}/{AspNetBaseImage}:{Net9ImageTag}"; + public const string FullyQualifiedBaseImageAspNetDigest = $"{BaseImageSource}/{AspNetBaseImage}@{Net9ImageDigest}"; private static string? s_registryContainerId; internal class SameArchManifestPicker : IManifestPicker @@ -77,8 +79,8 @@ public static async Task StartAndPopulateDockerRegistry(ITestOutputHelper testOu string dotnetdll = System.Reflection.Assembly.GetExecutingAssembly().Location; var ridjson = Path.Combine(Path.GetDirectoryName(dotnetdll)!, "RuntimeIdentifierGraph.json"); - var image = await pullRegistry.GetImageManifestAsync(RuntimeBaseImage, tag, "linux-x64", new SameArchManifestPicker(), CancellationToken.None); - var source = new SourceImageReference(pullRegistry, RuntimeBaseImage, tag); + var image = await pullRegistry.GetImageManifestAsync(RuntimeBaseImage, tag, "linux-x64", new SameArchManifestPicker(), CancellationToken.None); + var source = new SourceImageReference(pullRegistry, RuntimeBaseImage, tag, null); var dest = new DestinationImageReference(pushRegistry, RuntimeBaseImage, [tag]); logger.LogInformation($"Pushing image for {BaseImageSource}/{RuntimeBaseImage}:{tag}"); await pushRegistry.PushAsync(image.Build(), source, dest, CancellationToken.None); diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs index a992e06b4e8c..64404c07fed8 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs @@ -79,7 +79,7 @@ public async Task WriteToPrivateBasicRegistry() var ridgraphfile = ToolsetUtils.GetRuntimeGraphFilePath(); Registry mcr = new(DockerRegistryManager.BaseImageSource, logger, RegistryMode.Pull); - var sourceImage = new SourceImageReference(mcr, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net6ImageTag); + var sourceImage = new SourceImageReference(mcr, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net6ImageTag, null); var destinationImage = new DestinationImageReference(localAuthed, DockerRegistryManager.RuntimeBaseImage, new[] { DockerRegistryManager.Net6ImageTag }); ImageBuilder? downloadedImage = await mcr.GetImageManifestAsync( DockerRegistryManager.RuntimeBaseImage, diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs index 4267a39ebf25..4040d67eb522 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -82,7 +82,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull() BuiltImage builtImage = imageBuilder.Build(); // Push the image back to the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" }); await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false); @@ -128,7 +128,7 @@ public async Task ApiEndToEndWithLocalLoad() BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" }); await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); @@ -171,7 +171,7 @@ public async Task ApiEndToEndWithArchiveWritingAndLoad() // Write the image to disk var archiveFile = Path.Combine(TestSettings.TestArtifactsDirectory, nameof(ApiEndToEndWithArchiveWritingAndLoad), "app.tar.gz"); - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null); var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archiveFile), NewImageName(), new[] { "latest", "1.0" }); await destinationReference.LocalRegistry!.LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); @@ -618,8 +618,10 @@ public async Task EndToEnd_NoAPI_ProjectType(string projectType, bool addPackage privateNuGetAssets.Delete(true); } - [DockerAvailableFact] - public void EndToEnd_NoAPI_Console() + [DockerAvailableTheory()] + [InlineData(DockerRegistryManager.FullyQualifiedBaseImageAspNet)] + [InlineData(DockerRegistryManager.FullyQualifiedBaseImageAspNetDigest)] + public void EndToEnd_NoAPI_Console(string baseImage) { DirectoryInfo newProjectDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, "CreateNewImageTest")); DirectoryInfo privateNuGetAssets = new(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet")); @@ -672,7 +674,7 @@ public void EndToEnd_NoAPI_Console() "publish", "/t:PublishContainer", "/p:runtimeidentifier=linux-x64", - $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerBaseImage={baseImage}", $"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}", $"/p:ContainerRepository={imageName}", $"/p:ContainerImageTag={imageTag}", @@ -1212,7 +1214,11 @@ public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local registry +<<<<<<< HEAD var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); +======= + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag, null); +>>>>>>> 809df5c609 (Add support for using Digest for base image) var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { rid }); await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); diff --git a/test/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs b/test/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs index 9bec92972e33..27d2b7441bf4 100644 --- a/test/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs +++ b/test/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs @@ -23,29 +23,33 @@ public void IsValidRegistry(string registry, bool expectedReturn) } [Theory] - [InlineData("mcr.microsoft.com/dotnet/runtime:6.0", true, "mcr.microsoft.com", "dotnet/runtime", "6.0", true)] - [InlineData("mcr.microsoft.com/dotnet/runtime", true, "mcr.microsoft.com", "dotnet/runtime", null, true)] - [InlineData("mcr.microsoft.com/", false, null, null, null, false)] // no image = nothing resolves + [InlineData("mcr.microsoft.com/dotnet/runtime@sha256:6cec36412a215aad2a033cfe259890482be0a1dcb680e81fccc393b2d4069455", true, "mcr.microsoft.com", "dotnet/runtime", null, "sha256:6cec36412a215aad2a033cfe259890482be0a1dcb680e81fccc393b2d4069455", true)] + // Handle both tag and digest + [InlineData("mcr.microsoft.com/dotnet/runtime:6.0@sha256:6cec36412a215aad2a033cfe259890482be0a1dcb680e81fccc393b2d4069455", true, "mcr.microsoft.com", "dotnet/runtime", "6.0", "sha256:6cec36412a215aad2a033cfe259890482be0a1dcb680e81fccc393b2d4069455", true)] + [InlineData("mcr.microsoft.com/dotnet/runtime:6.0", true, "mcr.microsoft.com", "dotnet/runtime", "6.0", null, true)] + [InlineData("mcr.microsoft.com/dotnet/runtime", true, "mcr.microsoft.com", "dotnet/runtime", null, null, true)] + [InlineData("mcr.microsoft.com/", false, null, null, null, null, false)] // no image = nothing resolves // Ports tag along - [InlineData("mcr.microsoft.com:54/dotnet/runtime", true, "mcr.microsoft.com:54", "dotnet/runtime", null, true)] + [InlineData("mcr.microsoft.com:54/dotnet/runtime", true, "mcr.microsoft.com:54", "dotnet/runtime", null, null, true)] // Even if nonsensical - [InlineData("mcr.microsoft.com:0/dotnet/runtime", true, "mcr.microsoft.com:0", "dotnet/runtime", null, true)] + [InlineData("mcr.microsoft.com:0/dotnet/runtime", true, "mcr.microsoft.com:0", "dotnet/runtime", null, null, true)] // We don't allow hosts with missing ports when a port is anticipated - [InlineData("mcr.microsoft.com:/dotnet/runtime", false, null, null, null, false)] + [InlineData("mcr.microsoft.com:/dotnet/runtime", false, null, null, null, null, false)] // Use default registry when no registry specified. - [InlineData("ubuntu:jammy", true, DefaultRegistry, "library/ubuntu", "jammy", false)] - [InlineData("ubuntu/runtime:jammy", true, DefaultRegistry, "ubuntu/runtime", "jammy", false)] + [InlineData("ubuntu:jammy", true, DefaultRegistry, "library/ubuntu", "jammy", null, false)] + [InlineData("ubuntu/runtime:jammy", true, DefaultRegistry, "ubuntu/runtime", "jammy", null, false)] // Alias 'docker.io' to Docker registry. - [InlineData("docker.io/ubuntu:jammy", true, DefaultRegistry, "library/ubuntu", "jammy", true)] - [InlineData("docker.io/ubuntu/runtime:jammy", true, DefaultRegistry, "ubuntu/runtime", "jammy", true)] + [InlineData("docker.io/ubuntu:jammy", true, DefaultRegistry, "library/ubuntu", "jammy", null, true)] + [InlineData("docker.io/ubuntu/runtime:jammy", true, DefaultRegistry, "ubuntu/runtime", "jammy", null, true)] // 'localhost' registry. - [InlineData("localhost/ubuntu:jammy", true, "localhost", "ubuntu", "jammy", true)] - public void TryParseFullyQualifiedContainerName(string fullyQualifiedName, bool expectedReturn, string? expectedRegistry, string? expectedImage, string? expectedTag, bool expectedIsRegistrySpecified) + [InlineData("localhost/ubuntu:jammy", true, "localhost", "ubuntu", "jammy", null, true)] + public void TryParseFullyQualifiedContainerName(string fullyQualifiedName, bool expectedReturn, string? expectedRegistry, string? expectedImage, string? expectedTag, string? expectedDigest, bool expectedIsRegistrySpecified) { Assert.Equal(expectedReturn, ContainerHelpers.TryParseFullyQualifiedContainerName(fullyQualifiedName, out string? containerReg, out string? containerName, out string? containerTag, out string? containerDigest, out bool isRegistrySpecified)); Assert.Equal(expectedRegistry, containerReg); Assert.Equal(expectedImage, containerName); Assert.Equal(expectedTag, containerTag); + Assert.Equal(expectedDigest, containerDigest); Assert.Equal(expectedIsRegistrySpecified, isRegistrySpecified); } From c8cbf630a47564ee6dad46a0859854b37d3b52bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Sun, 27 Oct 2024 17:26:37 +0100 Subject: [PATCH 2/4] Alternative location for default tag --- .../Tasks/CreateNewImage.Interface.cs | 3 ++- .../Tasks/ParseContainerProperties.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 79dfa4961f6b..23b1c1cf9517 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -34,6 +34,7 @@ partial class CreateNewImage /// The base image tag. /// Ex: 6.0 /// + [Required] public string BaseImageTag { get; set; } /// @@ -191,7 +192,7 @@ public CreateNewImage() ToolPath = ""; BaseRegistry = ""; BaseImageName = ""; - BaseImageTag = "latest"; + BaseImageTag = ""; BaseImageDigest = ""; OutputRegistry = ""; ArchiveOutputPath = ""; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs index bab45e1c1ce1..26f22c49091a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs @@ -166,7 +166,8 @@ public override bool Execute() ParsedContainerRegistry = outputReg ?? ""; ParsedContainerImage = outputImage ?? ""; - ParsedContainerTag = outputTag ?? ""; + // If no Tag was provided, default to "latest" + ParsedContainerTag = outputTag ?? "latest"; ParsedContainerDigest = outputDigest ?? ""; NewContainerRegistry = ContainerRegistry; NewContainerTags = validTags; From 0324adbe35eab2714b0b59bfaa24ad65c65c7112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Mon, 28 Oct 2024 20:37:15 +0100 Subject: [PATCH 3/4] Refactor SourceImageReference --- .../ContainerBuilder.cs | 3 +-- .../SourceImageReference.cs | 12 +++++++++--- .../Tasks/CreateNewImage.Interface.cs | 1 - .../Tasks/CreateNewImage.cs | 3 +-- .../Tasks/ParseContainerProperties.cs | 3 +-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs index a89bf37b3321..751c697e6caf 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -63,11 +63,10 @@ internal static async Task ContainerizeAsync( { try { - string imageReference = !string.IsNullOrEmpty(baseImageDigest) ? baseImageDigest : baseImageTag; var ridGraphPicker = new RidGraphManifestPicker(ridGraphPath); imageBuilder = await registry.GetImageManifestAsync( baseImageName, - imageReference, + sourceImageReference.Reference, containerRuntimeIdentifier, ridGraphPicker, cancellationToken).ConfigureAwait(false); diff --git a/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs b/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs index 8a37f9b5ca8e..12fb19b82f07 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs @@ -6,17 +6,22 @@ namespace Microsoft.NET.Build.Containers; /// /// Represents a reference to a Docker image. A reference is made of a registry, a repository (aka the image name) and a tag or digest. /// -internal readonly record struct SourceImageReference(Registry? Registry, string Repository, string Tag, string? Digest) +internal readonly record struct SourceImageReference(Registry? Registry, string Repository, string? Tag, string? Digest) { public override string ToString() { - string sourceImageReference = RepositoryAndTag; + string sourceImageReference = Repository; if (Registry is { } reg) { sourceImageReference = $"{reg.RegistryName}/{sourceImageReference}"; } + if (!string.IsNullOrEmpty(Tag)) + { + sourceImageReference = $"{sourceImageReference}:{Tag}"; + } + if (!string.IsNullOrEmpty(Digest)) { sourceImageReference = $"{sourceImageReference}@{Digest}"; @@ -28,5 +33,6 @@ public override string ToString() /// /// Returns the repository and tag as a formatted string. Used in cases /// - public readonly string RepositoryAndTag => $"{Repository}:{Tag}"; + public string Reference + => !string.IsNullOrEmpty(Digest) ? Digest : !string.IsNullOrEmpty(Tag) ? Tag : "latest"; } 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 23b1c1cf9517..461ba622ccfc 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -34,7 +34,6 @@ partial class CreateNewImage /// The base image tag. /// Ex: 6.0 /// - [Required] public string BaseImageTag { 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 1a2a3ad3a036..4d81b963f4a7 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -79,11 +79,10 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) { try { - string imageReference = !string.IsNullOrEmpty(BaseImageDigest) ? BaseImageDigest : BaseImageTag; var picker = new RidGraphManifestPicker(RuntimeIdentifierGraphPath); imageBuilder = await registry.GetImageManifestAsync( BaseImageName, - imageReference, + sourceImageReference.Reference, ContainerRuntimeIdentifier, picker, cancellationToken).ConfigureAwait(false); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs index 26f22c49091a..bab45e1c1ce1 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs @@ -166,8 +166,7 @@ public override bool Execute() ParsedContainerRegistry = outputReg ?? ""; ParsedContainerImage = outputImage ?? ""; - // If no Tag was provided, default to "latest" - ParsedContainerTag = outputTag ?? "latest"; + ParsedContainerTag = outputTag ?? ""; ParsedContainerDigest = outputDigest ?? ""; NewContainerRegistry = ContainerRegistry; NewContainerTags = validTags; From e95d9ee9a3fa31186350dc673a81075985763709 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Dec 2024 09:49:52 -0600 Subject: [PATCH 4/4] add Digest null value to test scenario that was missed --- .../DockerRegistryManager.cs | 2 +- .../EndToEndTests.cs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs index 65e5f09b4cb6..bf9ae3ff2478 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs @@ -79,7 +79,7 @@ public static async Task StartAndPopulateDockerRegistry(ITestOutputHelper testOu string dotnetdll = System.Reflection.Assembly.GetExecutingAssembly().Location; var ridjson = Path.Combine(Path.GetDirectoryName(dotnetdll)!, "RuntimeIdentifierGraph.json"); - var image = await pullRegistry.GetImageManifestAsync(RuntimeBaseImage, tag, "linux-x64", new SameArchManifestPicker(), CancellationToken.None); + var image = await pullRegistry.GetImageManifestAsync(RuntimeBaseImage, tag, "linux-x64", new SameArchManifestPicker(), CancellationToken.None); var source = new SourceImageReference(pullRegistry, RuntimeBaseImage, tag, null); var dest = new DestinationImageReference(pushRegistry, RuntimeBaseImage, [tag]); logger.LogInformation($"Pushing image for {BaseImageSource}/{RuntimeBaseImage}:{tag}"); diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs index 4040d67eb522..f38433e3fcda 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -234,7 +234,7 @@ public async Task TarballsHaveCorrectStructure() BuiltImage builtImage = imageBuilder.Build(); // Write the image to disk - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net7ImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net7ImageTag, null); var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archiveFile), NewImageName(), tags); return (builtImage, sourceReference, destinationReference); @@ -1214,11 +1214,7 @@ public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local registry -<<<<<<< HEAD - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag); -======= - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag, null); ->>>>>>> 809df5c609 (Add support for using Digest for base image) + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { rid }); await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);