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 492027a1f41d..172358b16874 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/net472/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt index 6dd6551ddb37..5972149a0fca 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![]! @@ -111,6 +113,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 @@ -134,4 +137,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/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 8e7f42f35d3f..543be75b1dd7 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -148,6 +148,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 @@ -232,6 +234,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 @@ -284,4 +287,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.PlatformSpecificManifest[]! 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.PlatformSpecificManifest[]! manifests) -> void 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 4f69658c66d2..a74adeaa0845 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -37,6 +37,12 @@ partial class CreateNewImage [Required] public string BaseImageTag { get; set; } + /// + /// The base image digest. + /// Ex: sha256:12345... + /// + public string BaseImageDigest { get; set; } + /// /// The registry to push to. /// @@ -184,6 +190,7 @@ public CreateNewImage() BaseRegistry = ""; BaseImageName = ""; BaseImageTag = ""; + 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 e6537b3160af..dee12ce7b8b1 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..26f22c49091a 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); @@ -162,7 +166,9 @@ 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; @@ -172,6 +178,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 9f4796558438..857020f07035 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets @@ -119,6 +119,7 @@ + @@ -244,6 +245,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 66733c52896c..96eea015e986 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs @@ -240,7 +240,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 cc7aa1abc878..2a68e4b3b0d7 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs @@ -16,16 +16,18 @@ public class DockerRegistryManager public const string Net7ImageTag = "7.0"; public const string Net8ImageTag = "8.0"; public const string Net9PreviewImageTag = "9.0-preview"; + public const string Net9PreviewImageDigest = "sha256:8f530963f22ca799550c29580e4dfc3a674f57ba56267fc82670de773d9a2506"; public const string RuntimeFrameworkVersion = "9.0.0-preview.3.24172.9"; public const string Net8PreviewWindowsSpecificImageTag = $"{Net8ImageTag}-nanoserver-ltsc2022"; public const string LocalRegistry = "localhost:5010"; public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}/{RuntimeBaseImage}:{Net9PreviewImageTag}"; public const string FullyQualifiedBaseImageAspNet = $"{BaseImageSource}/{AspNetBaseImage}:{Net9PreviewImageTag}"; + public const string FullyQualifiedBaseImageAspNetDigest = $"{BaseImageSource}/{AspNetBaseImage}@{Net9PreviewImageDigest}"; private static string? s_registryContainerId; internal class SameArchManifestPicker : IManifestPicker { - public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary manifestList, string runtimeIdentifier) + public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary manifestList, string runtimeIdentifier) { return manifestList.Values.SingleOrDefault(m => m.platform.os == "linux" && m.platform.architecture == "amd64"); } @@ -74,7 +76,7 @@ public static async Task StartAndPopulateDockerRegistry(ITestOutputHelper testOu 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 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 8507aeff567b..0a979d48f3d4 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -66,7 +66,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull() BuiltImage builtImage = imageBuilder.Build(); // Push the image back to the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag, null); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" }); await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false); @@ -112,7 +112,7 @@ public async Task ApiEndToEndWithLocalLoad() BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag, null); var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" }); await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); @@ -155,7 +155,7 @@ public async Task ApiEndToEndWithArchiveWritingAndLoad() // Write the image to disk var archiveFile = Path.Combine(TestSettings.TestArtifactsDirectory, nameof(ApiEndToEndWithArchiveWritingAndLoad), "app.tar.gz"); - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag, null); var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archiveFile), NewImageName(), new[] { "latest", "1.0" }); await destinationReference.LocalRegistry!.LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); @@ -468,8 +468,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")); @@ -521,7 +523,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}", @@ -583,7 +585,7 @@ public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local registry - var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag); + var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag, null); 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); }