From ac3974d38f285e8886707a2c5f0143a5efeb3a3a Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 15 Feb 2023 19:04:55 +0100 Subject: [PATCH] `Image` refactoring and fixes to `ExposedPorts` and `Labels` logic (#341) --- .../EndToEndTests.cs | 40 ++- .../RegistryTests.cs | 2 +- .../ImageBuilderTests.cs | 294 ++++++++++++++++++ Microsoft.NET.Build.Containers/BuiltImage.cs | 50 +++ .../ContainerBuilder.cs | 24 +- Microsoft.NET.Build.Containers/DigestUtils.cs | 31 ++ Microsoft.NET.Build.Containers/Image.cs | 266 ---------------- .../ImageBuilder.cs | 86 +++++ Microsoft.NET.Build.Containers/ImageConfig.cs | 198 ++++++++++++ .../LocalDaemons/ILocalDaemon.cs | 2 +- .../LocalDaemons/LocalDocker.cs | 8 +- Microsoft.NET.Build.Containers/ManifestV2.cs | 47 ++- .../PublicAPI/net7.0/PublicAPI.Unshipped.txt | 18 +- Microsoft.NET.Build.Containers/Registry.cs | 26 +- .../Tasks/CreateNewImage.cs | 40 ++- docs/ReleaseNotes/v0.4.0.md | 1 + 16 files changed, 793 insertions(+), 340 deletions(-) create mode 100644 Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs create mode 100644 Microsoft.NET.Build.Containers/BuiltImage.cs create mode 100644 Microsoft.NET.Build.Containers/DigestUtils.cs delete mode 100644 Microsoft.NET.Build.Containers/Image.cs create mode 100644 Microsoft.NET.Build.Containers/ImageBuilder.cs create mode 100644 Microsoft.NET.Build.Containers/ImageConfig.cs diff --git a/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs index 64ace65c..59ddfe68 100644 --- a/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs +++ b/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -39,25 +39,27 @@ public async Task ApiEndToEndWithRegistryPushAndPull() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image? x = await registry.GetImageManifest( + ImageBuilder imageBuilder = await registry.GetImageManifest( DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", ToolsetUtils.GetRuntimeGraphFilePath()).ConfigureAwait(false); - Assert.NotNull(x); + Assert.NotNull(imageBuilder); Layer l = Layer.FromDirectory(publishDirectory, "/app"); - x.AddLayer(l); + imageBuilder.AddLayer(l); - x.SetEntrypoint(new[] { "/app/MinimalTestApp" }); + imageBuilder.SetEntryPoint(new[] { "/app/MinimalTestApp" }); + + BuiltImage builtImage = imageBuilder.Build(); // Push the image back to the local registry var sourceReference = new ImageReference(registry, DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag); var destinationReference = new ImageReference(registry, NewImageName(), "latest"); - await registry.Push(x, sourceReference, destinationReference, Console.WriteLine).ConfigureAwait(false); + await registry.Push(builtImage, sourceReference, destinationReference, Console.WriteLine).ConfigureAwait(false); // pull it back locally new BasicCommand(_testOutput, "docker", "pull", $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:latest") @@ -79,24 +81,26 @@ public async Task ApiEndToEndWithLocalLoad() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image? x = await registry.GetImageManifest( + ImageBuilder imageBuilder = await registry.GetImageManifest( DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", ToolsetUtils.GetRuntimeGraphFilePath()).ConfigureAwait(false); - Assert.NotNull(x); + Assert.NotNull(imageBuilder); Layer l = Layer.FromDirectory(publishDirectory, "/app"); - x.AddLayer(l); + imageBuilder.AddLayer(l); + + imageBuilder.SetEntryPoint(new[] { "/app/MinimalTestApp" }); - x.SetEntrypoint(new[] { "/app/MinimalTestApp" }); + BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local Docker daemon var sourceReference = new ImageReference(registry, DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag); var destinationReference = new ImageReference(registry, NewImageName(), "latest"); - await new LocalDocker(Console.WriteLine).Load(x, sourceReference, destinationReference).ConfigureAwait(false); + await new LocalDocker(Console.WriteLine).Load(builtImage, sourceReference, destinationReference).ConfigureAwait(false); // Run the image new BasicCommand(_testOutput, "docker", "run", "--rm", "--tty", $"{NewImageName()}:latest") @@ -289,21 +293,23 @@ public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDS // Build the image Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.BaseImageSource)); - Image? x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag, rid, ToolsetUtils.GetRuntimeGraphFilePath()).ConfigureAwait(false); - Assert.NotNull(x); + ImageBuilder? imageBuilder = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag, rid, ToolsetUtils.GetRuntimeGraphFilePath()).ConfigureAwait(false); + Assert.NotNull(imageBuilder); Layer l = Layer.FromDirectory(publishDirectory, "/app"); - x.AddLayer(l); - x.WorkingDirectory = workingDir; + imageBuilder.AddLayer(l); + imageBuilder.SetWorkingDirectory(workingDir); + + string[] entryPoint = DecideEntrypoint(rid, isRIDSpecific, "MinimalTestApp", workingDir); + imageBuilder.SetEntryPoint(entryPoint); - var entryPoint = DecideEntrypoint(rid, isRIDSpecific, "MinimalTestApp", workingDir); - x.SetEntrypoint(entryPoint); + BuiltImage builtImage = imageBuilder.Build(); // Load the image into the local Docker daemon var sourceReference = new ImageReference(registry, DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag); var destinationReference = new ImageReference(registry, NewImageName(), rid); - await new LocalDocker(Console.WriteLine).Load(x, sourceReference, destinationReference).ConfigureAwait(false); + await new LocalDocker(Console.WriteLine).Load(builtImage, sourceReference, destinationReference).ConfigureAwait(false); // Run the image new BasicCommand( diff --git a/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs b/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs index f9ee048f..9469f9cc 100644 --- a/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs +++ b/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs @@ -16,7 +16,7 @@ public async Task GetFromRegistry() // Don't need rid graph for local registry image pulls - since we're only pushing single image manifests (not manifest lists) // as part of our setup, we could put literally anything in here. The file at the passed-in path would only get read when parsing manifests lists. - Image? downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", ridgraphfile).ConfigureAwait(false); + ImageBuilder? downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", ridgraphfile).ConfigureAwait(false); Assert.NotNull(downloadedImage); } diff --git a/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs b/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs new file mode 100644 index 00000000..d25acf54 --- /dev/null +++ b/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.NET.Build.Containers; +using Xunit; + +namespace Test.Microsoft.NET.Build.Containers; + +public class ImageBuilderTests +{ + [Fact] + public void CanAddLabelsToImage() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.AddLabel("testLabel1", "v1"); + baseConfig.AddLabel("testLabel2", "v2"); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultLabels = result?["config"]?["Labels"] as JsonObject; + Assert.NotNull(resultLabels); + + Assert.Equal(2, resultLabels.Count); + Assert.Equal("v1", resultLabels["testLabel1"]?.ToString()); + Assert.Equal("v2", resultLabels["testLabel2"]?.ToString()); + } + + [Fact] + public void CanPreserveExistingLabels() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": + { + "existing" : "e1", + "existing2" : "e2" + } + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.AddLabel("testLabel1", "v1"); + baseConfig.AddLabel("existing2", "v2"); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultLabels = result?["config"]?["Labels"] as JsonObject; + Assert.NotNull(resultLabels); + + Assert.Equal(3, resultLabels.Count); + Assert.Equal("v1", resultLabels["testLabel1"]?.ToString()); + Assert.Equal("v2", resultLabels["existing2"]?.ToString()); + Assert.Equal("e1", resultLabels["existing"]?.ToString()); + } + + [Fact] + public void CanAddPortsToImage() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.ExposePort(6000, PortType.tcp); + baseConfig.ExposePort(6010, PortType.udp); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultPorts = result?["config"]?["ExposedPorts"] as JsonObject; + Assert.NotNull(resultPorts); + + Assert.Equal(2, resultPorts.Count); + Assert.NotNull(resultPorts["6000/tcp"] as JsonObject); + Assert.NotNull( resultPorts["6010/udp"] as JsonObject); + } + + [Fact] + public void CanPreserveExistingPorts() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null, + "ExposedPorts": + { + "6100/tcp": {}, + "6200": {} + } + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.ExposePort(6000, PortType.tcp); + baseConfig.ExposePort(6010, PortType.udp); + baseConfig.ExposePort(6100, PortType.udp); + baseConfig.ExposePort(6200, PortType.tcp); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultPorts = result?["config"]?["ExposedPorts"] as JsonObject; + Assert.NotNull(resultPorts); + + Assert.Equal(5, resultPorts.Count); + Assert.NotNull(resultPorts["6000/tcp"] as JsonObject); + Assert.NotNull(resultPorts["6010/udp"] as JsonObject); + Assert.NotNull(resultPorts["6100/udp"] as JsonObject); + Assert.NotNull(resultPorts["6100/tcp"] as JsonObject); + Assert.NotNull(resultPorts["6200/tcp"] as JsonObject); + } +} diff --git a/Microsoft.NET.Build.Containers/BuiltImage.cs b/Microsoft.NET.Build.Containers/BuiltImage.cs new file mode 100644 index 00000000..b3bb668f --- /dev/null +++ b/Microsoft.NET.Build.Containers/BuiltImage.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Build.Containers; + +/// +/// Represents constructed image ready for further processing. +/// +internal readonly struct BuiltImage +{ + /// + /// Gets image configuration in JSON format. + /// + internal required string Config { get; init; } + + /// + /// Gets image digest. + /// + internal required string ImageDigest { get; init; } + + /// + /// Gets image SHA. + /// + internal required string ImageSha { get; init; } + + /// + /// Gets image size. + /// + internal required long ImageSize { get; init; } + + /// + /// Gets image manifest. + /// + internal required ManifestV2 Manifest { get; init; } + + /// + /// Gets layers descriptors. + /// + internal IEnumerable LayerDescriptors + { + get + { + List layersNode = Manifest.Layers ?? throw new NotImplementedException("Tried to get layer information but there is no layer node?"); + foreach (ManifestLayer layer in layersNode) + { + yield return new(layer.mediaType, layer.digest, layer.size); + } + } + } +} diff --git a/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/Microsoft.NET.Build.Containers/ContainerBuilder.cs index a8b21058..763e0d61 100644 --- a/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -20,9 +20,9 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s var isDockerPush = String.IsNullOrEmpty(outputRegistry); var destinationImageReferences = imageTags.Select(t => new ImageReference(isDockerPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(outputRegistry!)), imageName, t)); - var img = await baseRegistry.GetImageManifest(baseName, baseTag, containerRuntimeIdentifier, ridGraphPath).ConfigureAwait(false); + ImageBuilder imageBuilder = await baseRegistry.GetImageManifest(baseName, baseTag, containerRuntimeIdentifier, ridGraphPath).ConfigureAwait(false); - img.WorkingDirectory = workingDir; + imageBuilder.SetWorkingDirectory(workingDir); JsonSerializerOptions options = new() { @@ -31,38 +31,40 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s Layer l = Layer.FromDirectory(folder.FullName, workingDir); - img.AddLayer(l); + imageBuilder.AddLayer(l); - img.SetEntrypoint(entrypoint, entrypointArgs); + imageBuilder.SetEntryPoint(entrypoint, entrypointArgs); - foreach (var label in labels) + foreach (string label in labels) { string[] labelPieces = label.Split('='); // labels are validated by System.CommandLine API - img.Label(labelPieces[0], labelPieces[1]); + imageBuilder.AddLabel(labelPieces[0], labelPieces[1]); } foreach (string envVar in envVars) { string[] envPieces = envVar.Split('=', 2); - img.AddEnvironmentVariable(envPieces[0], envPieces[1]); + imageBuilder.AddEnvironmentVariable(envPieces[0], envPieces[1]); } - foreach (var (number, type) in exposedPorts) + foreach ((int number, PortType type) in exposedPorts) { // ports are validated by System.CommandLine API - img.ExposePort(number, type); + imageBuilder.ExposePort(number, type); } + BuiltImage builtImage = imageBuilder.Build(); + foreach (var destinationImageReference in destinationImageReferences) { if (destinationImageReference.Registry is { } outReg) { try { - outReg.Push(img, sourceImageReference, destinationImageReference, (message) => Console.WriteLine($"Containerize: {message}")).Wait(); + outReg.Push(builtImage, sourceImageReference, destinationImageReference, (message) => Console.WriteLine($"Containerize: {message}")).Wait(); Console.WriteLine($"Containerize: Pushed container '{destinationImageReference.RepositoryAndTag}' to registry '{outputRegistry}'"); } catch (Exception e) @@ -83,7 +85,7 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s } try { - localDaemon.Load(img, sourceImageReference, destinationImageReference).Wait(); + localDaemon.Load(builtImage, sourceImageReference, destinationImageReference).Wait(); Console.WriteLine("Containerize: Pushed container '{0}' to Docker daemon", destinationImageReference.RepositoryAndTag); } catch (Exception e) diff --git a/Microsoft.NET.Build.Containers/DigestUtils.cs b/Microsoft.NET.Build.Containers/DigestUtils.cs new file mode 100644 index 00000000..a41b9a6f --- /dev/null +++ b/Microsoft.NET.Build.Containers/DigestUtils.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.NET.Build.Containers; + +internal sealed class DigestUtils +{ + /// + /// Gets digest for string . + /// + internal static string GetDigest(string str) => GetDigestFromSha(GetSha(str)); + + /// + /// Formats digest based on ready SHA . + /// + internal static string GetDigestFromSha(string sha) => $"sha256:{sha}"; + + /// + /// Gets the SHA of . + /// + internal static string GetSha(string str) + { + Span hash = stackalloc byte[SHA256.HashSizeInBytes]; + SHA256.HashData(Encoding.UTF8.GetBytes(str), hash); + + return Convert.ToHexString(hash).ToLowerInvariant(); + } +} diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs deleted file mode 100644 index f27350be..00000000 --- a/Microsoft.NET.Build.Containers/Image.cs +++ /dev/null @@ -1,266 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Microsoft.NET.Build.Containers; - -internal class Image -{ - private readonly HashSet