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