diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs index 48867a93de6b..751c697e6caf 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, @@ -65,14 +66,14 @@ internal static async Task ContainerizeAsync( var ridGraphPicker = new RidGraphManifestPicker(ridGraphPath); imageBuilder = await registry.GetImageManifestAsync( baseImageName, - baseImageTag, + sourceImageReference.Reference, 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..12fb19b82f07 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/SourceImageReference.cs @@ -4,24 +4,35 @@ 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 = Repository; + + if (Registry is { } reg) { - return $"{reg.RegistryName}/{Repository}:{Tag}"; - } - else + sourceImageReference = $"{reg.RegistryName}/{sourceImageReference}"; + } + + if (!string.IsNullOrEmpty(Tag)) { - return RepositoryAndTag; + sourceImageReference = $"{sourceImageReference}:{Tag}"; } + + if (!string.IsNullOrEmpty(Digest)) + { + sourceImageReference = $"{sourceImageReference}@{Digest}"; + } + + return sourceImageReference; } /// /// 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 3da5135b9b07..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,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. /// @@ -187,6 +192,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 34c4984ae0bb..4d81b963f4a7 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, @@ -82,7 +82,7 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) var picker = new RidGraphManifestPicker(RuntimeIdentifierGraphPath); imageBuilder = await registry.GetImageManifestAsync( BaseImageName, - BaseImageTag, + sourceImageReference.Reference, ContainerRuntimeIdentifier, picker, cancellationToken).ConfigureAwait(false); @@ -90,7 +90,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..bf9ae3ff2478 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 @@ -78,7 +80,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 4267a39ebf25..f38433e3fcda 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); @@ -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); @@ -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,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.Net9ImageTag); + 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); 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); }