From fdb756aa1774c18ec6270e2e7f35535b7dc5be17 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 11 Nov 2022 07:56:46 -0600 Subject: [PATCH 01/23] support manifest lists and use the RID to pick the most relevant manifest --- .../ContainerBuilder.cs | 101 ++++++++-------- .../CreateNewImage.Interface.cs | 9 ++ .../CreateNewImage.cs | 14 ++- Microsoft.NET.Build.Containers/Image.cs | 29 ++--- Microsoft.NET.Build.Containers/LocalDocker.cs | 4 +- Microsoft.NET.Build.Containers/Registry.cs | 114 ++++++++++++------ .../CreateNewImageTests.cs | 9 +- .../EndToEnd.cs | 4 +- .../RegistryTests.cs | 2 +- containerize/Program.cs | 8 +- .../Microsoft.NET.Build.Containers.targets | 3 +- 11 files changed, 179 insertions(+), 118 deletions(-) diff --git a/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/Microsoft.NET.Build.Containers/ContainerBuilder.cs index 008c7591..dd6b9583 100644 --- a/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -7,7 +7,7 @@ namespace Microsoft.NET.Build.Containers; public static class ContainerBuilder { - public static async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string[] entrypointArgs, string imageName, string[] imageTags, string outputRegistry, string[] labels, Port[] exposedPorts, string[] envVars) + public static async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string[] entrypointArgs, string imageName, string[] imageTags, string outputRegistry, string[] labels, Port[] exposedPorts, string[] envVars, string containerRuntimeIdentifier) { var isDockerPull = String.IsNullOrEmpty(registryName); if (isDockerPull) { @@ -15,73 +15,74 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s } Registry baseRegistry = new Registry(ContainerHelpers.TryExpandRegistryToUri(registryName)); - Image img = await baseRegistry.GetImageManifest(baseName, baseTag); - img.WorkingDirectory = workingDir; + var img = await baseRegistry.GetImageManifest(baseName, baseTag, containerRuntimeIdentifier); + if (img is not null) { + img.WorkingDirectory = workingDir; - JsonSerializerOptions options = new() - { - WriteIndented = true, - }; - - Layer l = Layer.FromDirectory(folder.FullName, workingDir); + JsonSerializerOptions options = new() + { + WriteIndented = true, + }; - img.AddLayer(l); + Layer l = Layer.FromDirectory(folder.FullName, workingDir); - img.SetEntrypoint(entrypoint, entrypointArgs); + img.AddLayer(l); - var isDockerPush = String.IsNullOrEmpty(outputRegistry); - Registry? outputReg = isDockerPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(outputRegistry)); + img.SetEntrypoint(entrypoint, entrypointArgs); - foreach (var label in labels) - { - string[] labelPieces = label.Split('='); + var isDockerPush = String.IsNullOrEmpty(outputRegistry); + Registry? outputReg = isDockerPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(outputRegistry)); - // labels are validated by System.CommandLine API - img.Label(labelPieces[0], labelPieces[1]); - } + foreach (var label in labels) + { + string[] labelPieces = label.Split('='); - foreach (string envVar in envVars) - { - string[] envPieces = envVar.Split('=', 2); + // labels are validated by System.CommandLine API + img.Label(labelPieces[0], labelPieces[1]); + } - img.AddEnvironmentVariable(envPieces[0], envPieces[1]); - } + foreach (string envVar in envVars) + { + string[] envPieces = envVar.Split('=', 2); - foreach (var (number, type) in exposedPorts) - { - // ports are validated by System.CommandLine API - img.ExposePort(number, type); - } + img.AddEnvironmentVariable(envPieces[0], envPieces[1]); + } - foreach (var tag in imageTags) - { - if (isDockerPush) + foreach (var (number, type) in exposedPorts) { - try - { - LocalDocker.Load(img, imageName, tag, baseName).Wait(); - Console.WriteLine("Containerize: Pushed container '{0}:{1}' to Docker daemon", imageName, tag); - } - catch (Exception e) - { - Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to local docker registry: {e}"); - Environment.ExitCode = -1; - } + // ports are validated by System.CommandLine API + img.ExposePort(number, type); } - else + + foreach (var tag in imageTags) { - try + if (isDockerPush) { - outputReg?.Push(img, imageName, tag, imageName, (message) => Console.WriteLine($"Containerize: {message}")).Wait(); - Console.WriteLine($"Containerize: Pushed container '{imageName}:{tag}' to registry '{outputRegistry}'"); + try + { + LocalDocker.Load(img, imageName, tag, baseName).Wait(); + Console.WriteLine("Containerize: Pushed container '{0}:{1}' to Docker daemon", imageName, tag); + } + catch (Exception e) + { + Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to local docker registry: {e}"); + Environment.ExitCode = -1; + } } - catch (Exception e) + else { - Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to output registry: {e}"); - Environment.ExitCode = -1; + try + { + outputReg?.Push(img, imageName, tag, imageName, (message) => Console.WriteLine($"Containerize: {message}")).Wait(); + Console.WriteLine($"Containerize: Pushed container '{imageName}:{tag}' to registry '{outputRegistry}'"); + } + catch (Exception e) + { + Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to output registry: {e}"); + Environment.ExitCode = -1; + } } } } - } } \ No newline at end of file diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs b/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs index 072c24af..cf366d05 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs @@ -93,6 +93,13 @@ partial class CreateNewImage /// public ITaskItem[] ContainerEnvironmentVariables { get; set; } + /// + /// The RID to use to determine the host manifest if the parent container is a manifest list + /// + [Required] + public string ContainerRuntimeIdentifier { get; set; } + + [Output] public string GeneratedContainerManifest { get; set; } @@ -117,6 +124,8 @@ public CreateNewImage() Labels = Array.Empty(); ExposedPorts = Array.Empty(); ContainerEnvironmentVariables = Array.Empty(); + ContainerRuntimeIdentifier = ""; + GeneratedContainerConfiguration = ""; GeneratedContainerManifest = ""; } diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index 40afe9fc..9b305510 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -1,4 +1,5 @@ -using Microsoft.Build.Framework; +using System.Text.Json; +using Microsoft.Build.Framework; namespace Microsoft.NET.Build.Containers.Tasks; @@ -72,12 +73,12 @@ private void SetEnvironmentVariables(Image img, ITaskItem[] envVars) } } - private Image GetBaseImage() { + private Image? GetBaseImage() { if (IsDockerPull) { throw new ArgumentException("Don't know how to pull images from local daemons at the moment"); } else { var reg = new Registry(ContainerHelpers.TryExpandRegistryToUri(BaseRegistry)); - return reg.GetImageManifest(BaseImageName, BaseImageTag).Result; + return reg.GetImageManifest(BaseImageName, BaseImageTag, ContainerRuntimeIdentifier).Result; } } @@ -95,6 +96,11 @@ public override bool Execute() var image = GetBaseImage(); + if (image is null) { + Log.LogError($"Couldn't find matching base image for {0}:{1} that matches RuntimeIdentifier {2}", BaseImageName, BaseImageTag, ContainerRuntimeIdentifier); + return !Log.HasLoggedErrors; + } + SafeLog("Building image '{0}' with tags {1} on top of base image {2}/{3}:{4}", ImageName, String.Join(",", ImageTags), BaseRegistry, BaseImageName, BaseImageTag); Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory); @@ -118,7 +124,7 @@ public override bool Execute() } // at this point we're done with modifications and are just pushing the data other places - GeneratedContainerManifest = image.manifest.ToJsonString(); + GeneratedContainerManifest = JsonSerializer.Serialize(image.manifest); GeneratedContainerConfiguration = image.config.ToJsonString(); Registry? outputReg = IsDockerPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(OutputRegistry)); diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs index af2af13a..aaae7486 100644 --- a/Microsoft.NET.Build.Containers/Image.cs +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -8,7 +8,7 @@ namespace Microsoft.NET.Build.Containers; public class Image { - public JsonNode manifest; + public ManifestV2 manifest; public JsonNode config; public readonly string OriginatingName; @@ -22,7 +22,7 @@ public class Image internal Dictionary environmentVariables; - public Image(JsonNode manifest, JsonNode config, string name, Registry? registry) + public Image(ManifestV2 manifest, JsonNode config, string name, Registry? registry) { this.manifest = manifest; this.config = config; @@ -38,21 +38,16 @@ public IEnumerable LayerDescriptors { get { - JsonNode? layersNode = manifest["layers"]; + var layersNode = manifest.layers; if (layersNode is null) { throw new NotImplementedException("Tried to get layer information but there is no layer node?"); } - foreach (JsonNode? descriptorJson in layersNode.AsArray()) + foreach (var layer in layersNode) { - if (descriptorJson is null) - { - throw new NotImplementedException("Null layer descriptor in the list?"); - } - - yield return descriptorJson.Deserialize(); + yield return new(layer.mediaType, layer.digest, layer.size); } } } @@ -60,7 +55,8 @@ public IEnumerable LayerDescriptors public void AddLayer(Layer l) { newLayers.Add(l); - manifest["layers"]!.AsArray().Add(l.Descriptor); + + manifest.layers.Add(new (l.Descriptor.MediaType, l.Descriptor.Size, l.Descriptor.Digest, l.Descriptor.Urls)); config["rootfs"]!["diff_ids"]!.AsArray().Add(l.Descriptor.UncompressedDigest); RecalculateDigest(); } @@ -68,9 +64,7 @@ public void AddLayer(Layer l) private void RecalculateDigest() { config["created"] = DateTime.UtcNow; - - manifest["config"]!["digest"] = GetDigest(config); - manifest["config"]!["size"] = Encoding.UTF8.GetBytes(config.ToJsonString()).Length; + manifest = manifest with { config = manifest.config with { digest = GetDigest(config), size = Encoding.UTF8.GetBytes(config.ToJsonString()).Length } }; } private JsonObject CreatePortMap() @@ -249,6 +243,13 @@ public string GetDigest(JsonNode json) return $"sha256:{hashString}"; } + public string GetDigest(T item) + { + var node = JsonSerializer.SerializeToNode(item); + if (node is not null) return GetDigest(node); + else return String.Empty; + } + public static string GetSha(JsonNode json) { Span hash = stackalloc byte[SHA256.HashSizeInBytes]; diff --git a/Microsoft.NET.Build.Containers/LocalDocker.cs b/Microsoft.NET.Build.Containers/LocalDocker.cs index 7346f710..2a151ad5 100644 --- a/Microsoft.NET.Build.Containers/LocalDocker.cs +++ b/Microsoft.NET.Build.Containers/LocalDocker.cs @@ -47,12 +47,12 @@ public static async Task WriteImageToStream(Image x, string name, string tag, St foreach (var d in x.LayerDescriptors) { - if (!x.originatingRegistry.HasValue) + if (x.originatingRegistry is null) { throw new NotImplementedException("Need a good error for 'couldn't download a thing because no link to registry'"); } - string localPath = await x.originatingRegistry.Value.DownloadBlob(x.OriginatingName, d); + string localPath = await x.originatingRegistry.DownloadBlob(x.OriginatingName, d); // Stuff that (uncompressed) tarball into the image tar stream // TODO uncompress!! diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index 2d8462e6..e29f3b79 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -8,52 +8,90 @@ using System.Net.Http.Json; using System.Runtime.ExceptionServices; using System.Text; +using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Xml.Linq; namespace Microsoft.NET.Build.Containers; -public record struct Registry(Uri BaseUri) +public record struct ManifestConfig(string mediaType, long size, string digest); +public record struct ManifestLayer(string mediaType, long size, string digest, string[]? urls); +public record struct ManifestV2(int schemaVersion, string tag, string mediaType, ManifestConfig config, List layers); + +// not a complete list, only the subset that we support +// public enum GoOS { linux, windows }; +// not a complete list, only the subset that we support +// public enum GoArch { amd64, arm , arm64, [JsonStringEnumMember("386")] x386 }; +public record struct PlatformInformation(string architecture, string os, string? variant, string[] features); +public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform); +public record struct ManifestListV2(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests); + +public record Registry(Uri BaseUri) { private const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json"; + private const string DockerManifestListV2 = "application/vnd.docker.distribution.manifest.list.v2+json"; private const string DockerContainerV1 = "application/vnd.docker.container.image.v1+json"; private const int MaxChunkSizeBytes = 1024 * 64; private string RegistryName { get; } = BaseUri.Host; - public async Task GetImageManifest(string name, string reference) + public async Task GetImageManifest(string name, string reference, string runtimeIdentifier) { - HttpClient client = GetClient(); - - var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{reference}")); - - response.EnsureSuccessStatusCode(); - - var s = await response.Content.ReadAsStringAsync(); - - var manifest = JsonNode.Parse(s); - - if (manifest is null) throw new NotImplementedException("Got a manifest but it was null"); + using var client = GetClient(); + var initialManifestResponse = await GetManifest(reference); + + return initialManifestResponse.Content.Headers.ContentType?.MediaType switch { + DockerManifestV2 => await TryReadSingleImage(await initialManifestResponse.Content.ReadFromJsonAsync()), + DockerManifestListV2 => await TryPickBestImageFromManifestList(await initialManifestResponse.Content.ReadFromJsonAsync(), runtimeIdentifier), + var unknownMediaType => throw new NotImplementedException($"Do not understand the mediaType {unknownMediaType}") + }; + + async Task GetManifest(string reference) { + using var client = GetClient(); + var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{reference}")); + response.EnsureSuccessStatusCode(); + return response; + } - if ((string?)manifest["mediaType"] != DockerManifestV2) - { - throw new NotImplementedException($"Do not understand the mediaType {manifest["mediaType"]}"); + async Task GetBlob(string digest) { + using var client = GetClient(); + var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/blobs/{digest}")); + response.EnsureSuccessStatusCode(); + return response; } - JsonNode? config = manifest["config"]; - Debug.Assert(config is not null); - Debug.Assert(((string?)config["mediaType"]) == DockerContainerV1); + async Task TryReadSingleImage(ManifestV2 manifest) { + var config = manifest.config; + string configSha = config.digest; + + var blobResponse = await GetBlob(configSha); - string? configSha = (string?)config["digest"]; - Debug.Assert(configSha is not null); + JsonNode? configDoc = JsonNode.Parse(await blobResponse.Content.ReadAsStringAsync()); + Debug.Assert(configDoc is not null); - response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/blobs/{configSha}")); + return new Image(manifest, configDoc, name, this); + } - JsonNode? configDoc = JsonNode.Parse(await response.Content.ReadAsStringAsync()); - Debug.Assert(configDoc is not null); - //Debug.Assert(((string?)configDoc["mediaType"]) == DockerContainerV1); + async Task TryPickBestImageFromManifestList(ManifestListV2 manifestList, string runtimeIdentifier) { + (string os, string arch, string? variant) = runtimeIdentifier.Split('-') switch { + ["linux", "x64"] => ("linux", "amd64", null), + ["linux", "x86"] => ("linux", "386", null), + ["linux", "arm"] => ("linux", "arm", "v7"), + ["linux", "arm64"] => ("linux", "arm64", "v8"), + ["windows", "x64"] => ("windows", "amd64", null), + var parts => throw new ArgumentException($"Unknown OS/platform combination {String.Join(' ', parts)}") + }; + + var potentialManifest = manifestList.manifests.SingleOrDefault(manifest => manifest.platform.os == os && manifest.platform.architecture == arch && manifest.platform.variant == variant); + if (potentialManifest != default) { + var manifestResponse = await GetManifest(potentialManifest.digest); + return await TryReadSingleImage(await manifestResponse.Content.ReadFromJsonAsync()); + } else { + return null; + } + } - return new Image(manifest, configDoc, name, this); } /// @@ -103,7 +141,7 @@ public async Task Push(Layer layer, string name, Action logProgressMessa } } - private readonly async Task UploadBlob(string name, string digest, Stream contents) + private async Task UploadBlob(string name, string digest, Stream contents) { HttpClient client = GetClient(); @@ -118,7 +156,7 @@ private readonly async Task UploadBlob(string name, string digest, Stream conten if (pushResponse.StatusCode != HttpStatusCode.Accepted) { - string errorMessage = $"Failed to upload blob to {pushUri}; recieved {pushResponse.StatusCode} with detail {await pushResponse.Content.ReadAsStringAsync()}"; + string errorMessage = $"Failed to upload blob to {pushUri}; received {pushResponse.StatusCode} with detail {await pushResponse.Content.ReadAsStringAsync()}"; throw new ApplicationException(errorMessage); } @@ -199,7 +237,7 @@ private readonly async Task UploadBlob(string name, string digest, Stream conten } } - private readonly async Task BlobAlreadyUploaded(string name, string digest, HttpClient client) + private async Task BlobAlreadyUploaded(string name, string digest, HttpClient client) { HttpResponseMessage response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, new Uri(BaseUri, $"/v2/{name}/blobs/{digest}"))); @@ -224,12 +262,10 @@ private static HttpClient CreateClient() HttpClient client = new(clientHandler); client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue("application/json")); - client.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue(DockerManifestV2)); - client.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue(DockerContainerV1)); + client.DefaultRequestHeaders.Accept.Add(new("application/json")); + client.DefaultRequestHeaders.Accept.Add(new(DockerManifestListV2)); + client.DefaultRequestHeaders.Accept.Add(new(DockerManifestV2)); + client.DefaultRequestHeaders.Accept.Add(new(DockerContainerV1)); client.DefaultRequestHeaders.Add("User-Agent", ".NET Container Library"); @@ -258,13 +294,13 @@ await Task.WhenAll(x.LayerDescriptors.Select(async descriptor => { { // The blob wasn't already available in another namespace, so fall back to explicitly uploading it - if (!x.originatingRegistry.HasValue) + if (x.originatingRegistry is null) { throw new NotImplementedException("Need a good error for 'couldn't download a thing because no link to registry'"); } // Ensure the blob is available locally - await x.originatingRegistry.Value.DownloadBlob(x.OriginatingName, descriptor); + await x.originatingRegistry.DownloadBlob(x.OriginatingName, descriptor); // Then push it to the destination registry await reg.Push(Layer.FromDescriptor(descriptor), name, logProgressMessage); logProgressMessage($"Finished uploading layer {digest} to {reg.RegistryName}"); @@ -280,8 +316,8 @@ await Task.WhenAll(x.LayerDescriptors.Select(async descriptor => { } var manifestDigest = x.GetDigest(x.manifest); - logProgressMessage($"Uploading manifest to {RegistryName} as blob {manifestDigest}"); - string jsonString = x.manifest.ToJsonString(); + logProgressMessage($"Uploading manifest to registry {RegistryName} as blob {manifestDigest}"); + string jsonString = JsonSerializer.SerializeToNode(x.manifest)?.ToJsonString() ?? ""; HttpContent manifestUploadContent = new StringContent(jsonString); manifestUploadContent.Headers.ContentType = new MediaTypeHeaderValue(DockerManifestV2); var putResponse = await client.PutAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{manifestDigest}"), manifestUploadContent); diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs index 6a35c1ff..5419bd9f 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs @@ -34,7 +34,7 @@ public void CreateNewImage_Baseline() dotnetNew.WaitForExit(); Assert.AreEqual(0, dotnetNew.ExitCode); - info.Arguments = "build --configuration release"; + info.Arguments = "publish -c Release -r linux-arm64"; Process dotnetPublish = Process.Start(info); Assert.IsNotNull(dotnetPublish); @@ -44,13 +44,14 @@ public void CreateNewImage_Baseline() CreateNewImage task = new CreateNewImage(); task.BaseRegistry = "mcr.microsoft.com"; task.BaseImageName = "dotnet/runtime"; - task.BaseImageTag = "6.0"; + task.BaseImageTag = "7.0"; task.OutputRegistry = "localhost:5010"; - task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", "net7.0"); + task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "Release", "net7.0", "linux-arm64", "publish"); task.ImageName = "dotnet/testimage"; task.ImageTags = new[] { "latest" }; task.WorkingDirectory = "app/"; + task.ContainerRuntimeIdentifier = "linux-arm64"; task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") }; Assert.IsTrue(task.Execute()); @@ -114,6 +115,7 @@ public void ParseContainerProperties_EndToEnd() cni.WorkingDirectory = "app/"; cni.Entrypoint = new TaskItem[] { new("ParseContainerProperties_EndToEnd") }; cni.ImageTags = pcp.NewContainerTags; + cni.ContainerRuntimeIdentifier = "linux-x64"; Assert.IsTrue(cni.Execute()); newProjectDir.Delete(true); @@ -190,6 +192,7 @@ public void Tasks_EndToEnd_With_EnvironmentVariable_Validation() cni.Entrypoint = new TaskItem[] { new("/app/Tasks_EndToEnd_With_EnvironmentVariable_Validation") }; cni.ImageTags = pcp.NewContainerTags; cni.ContainerEnvironmentVariables = pcp.NewContainerEnvironmentVariables; + cni.ContainerRuntimeIdentifier = "linux-x64"; Assert.IsTrue(cni.Execute()); diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index d3788781..06d29f74 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -31,7 +31,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag); + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag, "linux-x64"); Layer l = Layer.FromDirectory(publishDirectory, "/app"); @@ -69,7 +69,7 @@ public async Task ApiEndToEndWithLocalLoad() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag); + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag, "linux-x64"); Layer l = Layer.FromDirectory(publishDirectory, "/app"); diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs index adc8fb35..ee98038b 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs @@ -15,7 +15,7 @@ public async Task GetFromRegistry() { Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag); + Image downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag, "linux-x64"); Assert.IsNotNull(downloadedImage); } diff --git a/containerize/Program.cs b/containerize/Program.cs index 95075e57..71a9de35 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -151,6 +151,8 @@ AllowMultipleArgumentsPerToken = true }; +var ridOpt = new Option(name: "--rid", description: "Runtime Identifier of the generated container."); + RootCommand root = new RootCommand("Containerize an application without Docker.") { @@ -166,7 +168,8 @@ entrypointArgsOpt, labelsOpt, portsOpt, - envVarsOpt + envVarsOpt, + ridOpt }; root.SetHandler(async (context) => @@ -184,7 +187,8 @@ string[] _labels = context.ParseResult.GetValueForOption(labelsOpt) ?? Array.Empty(); Port[] _ports = context.ParseResult.GetValueForOption(portsOpt) ?? Array.Empty(); string[] _envVars = context.ParseResult.GetValueForOption(envVarsOpt) ?? Array.Empty(); - await ContainerBuilder.Containerize(_publishDir, _workingDir, _baseReg, _baseName, _baseTag, _entrypoint, _entrypointArgs, _name, _tags, _outputReg, _labels, _ports, _envVars); + string _rid = context.ParseResult.GetValueForOption(ridOpt) ?? ""; + await ContainerBuilder.Containerize(_publishDir, _workingDir, _baseReg, _baseName, _baseTag, _entrypoint, _entrypointArgs, _name, _tags, _outputReg, _labels, _ports, _envVars, _rid); }); return await root.InvokeAsync(args); diff --git a/packaging/build/Microsoft.NET.Build.Containers.targets b/packaging/build/Microsoft.NET.Build.Containers.targets index b49973d5..796a69d5 100644 --- a/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/packaging/build/Microsoft.NET.Build.Containers.targets @@ -136,7 +136,8 @@ EntrypointArgs="@(ContainerEntrypointArgs)" Labels="@(ContainerLabel)" ExposedPorts="@(ContainerPort)" - ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)"> + ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)" + ContainerRuntimeIdentifier="$(RuntimeIdentifier)"> From 5209f491450d675ea7487a2aef70c440a2000f09 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 11 Nov 2022 08:27:03 -0600 Subject: [PATCH 02/23] don't dispose the client --- Microsoft.NET.Build.Containers/Registry.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index e29f3b79..3fe4dd43 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -38,7 +38,7 @@ public record Registry(Uri BaseUri) public async Task GetImageManifest(string name, string reference, string runtimeIdentifier) { - using var client = GetClient(); + var client = GetClient(); var initialManifestResponse = await GetManifest(reference); return initialManifestResponse.Content.Headers.ContentType?.MediaType switch { @@ -48,14 +48,14 @@ public record Registry(Uri BaseUri) }; async Task GetManifest(string reference) { - using var client = GetClient(); + var client = GetClient(); var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{reference}")); response.EnsureSuccessStatusCode(); return response; } async Task GetBlob(string digest) { - using var client = GetClient(); + var client = GetClient(); var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/blobs/{digest}")); response.EnsureSuccessStatusCode(); return response; From 0468080cad4a3cf2f66912f2d9d7b3737c7aaec0 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 21 Nov 2022 14:41:16 -0600 Subject: [PATCH 03/23] write tests for packaging an app in all supported configurations --- Microsoft.NET.Build.Containers/Registry.cs | 6 +- .../DockerRegistryManager.cs | 13 ++-- .../EndToEnd.cs | 73 ++++++++++++++++--- .../RegistryTests.cs | 2 +- 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index 3fe4dd43..ea3c406a 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -74,12 +74,16 @@ async Task GetBlob(string digest) { } async Task TryPickBestImageFromManifestList(ManifestListV2 manifestList, string runtimeIdentifier) { + // TODO: we probably need to pull in actual RID parsing code and look for 'platform' here. + // 'win' can take a version number and we'd break. + // Also, there are more specific linux RIDs (like rhel) that we should instead be looking for the correct 'family' for? + // we probably also need to look at the 'variant' field if the RID contains a version. (string os, string arch, string? variant) = runtimeIdentifier.Split('-') switch { ["linux", "x64"] => ("linux", "amd64", null), ["linux", "x86"] => ("linux", "386", null), ["linux", "arm"] => ("linux", "arm", "v7"), ["linux", "arm64"] => ("linux", "arm64", "v8"), - ["windows", "x64"] => ("windows", "amd64", null), + ["win", "x64"] => ("windows", "amd64", null), var parts => throw new ArgumentException($"Unknown OS/platform combination {String.Join(' ', parts)}") }; diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs index a3dbc1a7..5178e6a4 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs @@ -8,9 +8,10 @@ public class DockerRegistryManager { public const string BaseImage = "dotnet/runtime"; public const string BaseImageSource = "mcr.microsoft.com/"; - public const string BaseImageTag = "6.0"; + public const string Net6ImageTag = "6.0"; + public const string Net7ImageTag = "7.0"; public const string LocalRegistry = "localhost:5010"; - public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}{BaseImage}:{BaseImageTag}"; + public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}{BaseImage}:{Net6ImageTag}"; private static string s_registryContainerId; private static void Exec(string command, string args) { @@ -53,9 +54,11 @@ public static void StartAndPopulateDockerRegistry(TestContext context) s_registryContainerId = registryContainerId; - Exec("docker", $"pull {BaseImageSource}{BaseImage}:{BaseImageTag}"); - Exec("docker", $"tag {BaseImageSource}{BaseImage}:{BaseImageTag} {LocalRegistry}/{BaseImage}:{BaseImageTag}"); - Exec("docker", $"push {LocalRegistry}/{BaseImage}:{BaseImageTag}"); + foreach (var tag in new [] { Net6ImageTag, Net7ImageTag}) { + Exec("docker", $"pull {BaseImageSource}{BaseImage}:{tag}"); + Exec("docker", $"tag {BaseImageSource}{BaseImage}:{tag} {LocalRegistry}/{BaseImage}:{tag}"); + Exec("docker", $"push {LocalRegistry}/{BaseImage}:{tag}"); + } LocateMSBuild(); } diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index 06d29f74..02b06f99 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -31,7 +31,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag, "linux-x64"); + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64"); Layer l = Layer.FromDirectory(publishDirectory, "/app"); @@ -69,7 +69,7 @@ public async Task ApiEndToEndWithLocalLoad() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag, "linux-x64"); + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64"); Layer l = Layer.FromDirectory(publishDirectory, "/app"); @@ -91,7 +91,7 @@ public async Task ApiEndToEndWithLocalLoad() Assert.AreEqual(0, run.ExitCode); } - private static async Task BuildLocalApp() + private static async Task BuildLocalApp(string tfm = "net6.0", string rid = "linux-x64") { DirectoryInfo d = new DirectoryInfo("MinimalTestApp"); if (d.Exists) @@ -99,7 +99,7 @@ private static async Task BuildLocalApp() d.Delete(recursive: true); } - ProcessStartInfo psi = new("dotnet", "new console -f net6.0 -o MinimalTestApp") + ProcessStartInfo psi = new("dotnet", $"new console -f {tfm} -o MinimalTestApp") { RedirectStandardOutput = true, RedirectStandardError = true, @@ -109,16 +109,17 @@ private static async Task BuildLocalApp() Assert.IsNotNull(dotnetNew); await dotnetNew.WaitForExitAsync(); - Assert.AreEqual(0, dotnetNew.ExitCode, await dotnetNew.StandardOutput.ReadToEndAsync() + await dotnetNew.StandardError.ReadToEndAsync()); + Assert.AreEqual(0, dotnetNew.ExitCode, await dotnetNew.StandardOutput.ReadToEndAsync() + Environment.NewLine + await dotnetNew.StandardError.ReadToEndAsync()); - // Build project - - Process publish = Process.Start("dotnet", "publish -bl MinimalTestApp -r linux-x64"); + ProcessStartInfo publishPSI = rid is null ? new("dotnet", $"publish -bl MinimalTestApp") : new("dotnet", $"publish -bl MinimalTestApp -r {rid}"); + publishPSI.RedirectStandardOutput = true; + publishPSI.RedirectStandardError = true; + Process publish = Process.Start(publishPSI); Assert.IsNotNull(publish); await publish.WaitForExitAsync(); - Assert.AreEqual(0, publish.ExitCode); + Assert.AreEqual(0, publish.ExitCode, await publish.StandardOutput.ReadToEndAsync() + Environment.NewLine + await publish.StandardError.ReadToEndAsync()); - string publishDirectory = Path.Join("MinimalTestApp", "bin", "Debug", "net6.0", "linux-x64", "publish"); + string publishDirectory = Path.Join("MinimalTestApp", "bin", "Debug", tfm, rid, "publish"); return publishDirectory; } @@ -274,4 +275,56 @@ public async Task EndToEnd_NoAPI() newProjectDir.Delete(true); privateNuGetAssets.Delete(true); } + + [DataRow("linux-x86", false, "/app")] // packaging framework-dependent because missing runtime packs for x86 linux. + [DataRow("linux-x64", true, "/app")] + [DataRow("linux-arm", false, "/app")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work + [DataRow("linux-arm64", false, "/app")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work + [DataRow("win-x64", true, "C:\\app")] + [TestMethod] + public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDSpecific, string workingDir) { + if (rid == "win-x64") { + Assert.Inconclusive("Cannot run Windows containers on Linux hosts (or at the same time as Linux containers), so skipping for now"); + return; + } + string publishDirectory = await BuildLocalApp(tfm : "net7.0", rid : (isRIDSpecific ? rid : null)); + + // Build the image + Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); + + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag, rid); + + Layer l = Layer.FromDirectory(publishDirectory, "/app"); + + x.AddLayer(l); + x.WorkingDirectory = workingDir; + + var entryPoint = DecideEntrypoint(rid, isRIDSpecific, "MinimalTestApp", workingDir); + x.SetEntrypoint(entryPoint); + + // Load the image into the local Docker daemon + + await LocalDocker.Load(x, NewImageName(), rid, DockerRegistryManager.BaseImage); + + // Run the image + + ProcessStartInfo runInfo = new("docker", $"run --rm --tty {NewImageName()}:{rid}") { + RedirectStandardError = true, + RedirectStandardOutput = true, + }; + Process run = Process.Start(runInfo); + Assert.IsNotNull(run); + await run.WaitForExitAsync(); + + Assert.AreEqual(0, run.ExitCode, run.StandardOutput.ReadToEnd() + Environment.NewLine + run.StandardError.ReadToEnd()); + + string[] DecideEntrypoint(string rid, bool isRIDSpecific, string appName, string workingDir) { + var binary = rid.StartsWith("win") ? $"{appName}.exe" : appName; + if (isRIDSpecific) { + return new[] { $"{workingDir}/{binary}" }; + } else { + return new[] { "dotnet", $"{workingDir}/{binary}.dll" }; + } + } + } } \ No newline at end of file diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs index ee98038b..66498a6d 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs @@ -15,7 +15,7 @@ public async Task GetFromRegistry() { Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.BaseImageTag, "linux-x64"); + Image downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64"); Assert.IsNotNull(downloadedImage); } From c75bf13922b2cd8f093943987a42f8f186922d36 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 21 Nov 2022 15:05:27 -0600 Subject: [PATCH 04/23] more thought put into error messages --- .../ContainerBuilder.cs | 100 +++++++++--------- Microsoft.NET.Build.Containers/Image.cs | 9 +- Microsoft.NET.Build.Containers/Registry.cs | 14 ++- 3 files changed, 69 insertions(+), 54 deletions(-) diff --git a/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/Microsoft.NET.Build.Containers/ContainerBuilder.cs index dd6b9583..488f5607 100644 --- a/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -16,71 +16,73 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s Registry baseRegistry = new Registry(ContainerHelpers.TryExpandRegistryToUri(registryName)); var img = await baseRegistry.GetImageManifest(baseName, baseTag, containerRuntimeIdentifier); - if (img is not null) { - img.WorkingDirectory = workingDir; + if (img is null) { + throw new ArgumentException($"Could not find image {baseName}:{baseTag} in registry {registryName} matching RuntimeIdentifier {containerRuntimeIdentifier}"); + } - JsonSerializerOptions options = new() - { - WriteIndented = true, - }; + img.WorkingDirectory = workingDir; - Layer l = Layer.FromDirectory(folder.FullName, workingDir); + JsonSerializerOptions options = new() + { + WriteIndented = true, + }; - img.AddLayer(l); + Layer l = Layer.FromDirectory(folder.FullName, workingDir); - img.SetEntrypoint(entrypoint, entrypointArgs); + img.AddLayer(l); - var isDockerPush = String.IsNullOrEmpty(outputRegistry); - Registry? outputReg = isDockerPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(outputRegistry)); + img.SetEntrypoint(entrypoint, entrypointArgs); - foreach (var label in labels) - { - string[] labelPieces = label.Split('='); + var isDockerPush = String.IsNullOrEmpty(outputRegistry); + Registry? outputReg = isDockerPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(outputRegistry)); - // labels are validated by System.CommandLine API - img.Label(labelPieces[0], labelPieces[1]); - } + foreach (var label in labels) + { + string[] labelPieces = label.Split('='); - foreach (string envVar in envVars) - { - string[] envPieces = envVar.Split('=', 2); + // labels are validated by System.CommandLine API + img.Label(labelPieces[0], labelPieces[1]); + } - img.AddEnvironmentVariable(envPieces[0], envPieces[1]); - } + foreach (string envVar in envVars) + { + string[] envPieces = envVar.Split('=', 2); + + img.AddEnvironmentVariable(envPieces[0], envPieces[1]); + } + + foreach (var (number, type) in exposedPorts) + { + // ports are validated by System.CommandLine API + img.ExposePort(number, type); + } - foreach (var (number, type) in exposedPorts) + foreach (var tag in imageTags) + { + if (isDockerPush) { - // ports are validated by System.CommandLine API - img.ExposePort(number, type); + try + { + LocalDocker.Load(img, imageName, tag, baseName).Wait(); + Console.WriteLine("Containerize: Pushed container '{0}:{1}' to Docker daemon", imageName, tag); + } + catch (Exception e) + { + Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to local docker registry: {e}"); + Environment.ExitCode = -1; + } } - - foreach (var tag in imageTags) + else { - if (isDockerPush) + try { - try - { - LocalDocker.Load(img, imageName, tag, baseName).Wait(); - Console.WriteLine("Containerize: Pushed container '{0}:{1}' to Docker daemon", imageName, tag); - } - catch (Exception e) - { - Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to local docker registry: {e}"); - Environment.ExitCode = -1; - } + outputReg?.Push(img, imageName, tag, imageName, (message) => Console.WriteLine($"Containerize: {message}")).Wait(); + Console.WriteLine($"Containerize: Pushed container '{imageName}:{tag}' to registry '{outputRegistry}'"); } - else + catch (Exception e) { - try - { - outputReg?.Push(img, imageName, tag, imageName, (message) => Console.WriteLine($"Containerize: {message}")).Wait(); - Console.WriteLine($"Containerize: Pushed container '{imageName}:{tag}' to registry '{outputRegistry}'"); - } - catch (Exception e) - { - Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to output registry: {e}"); - Environment.ExitCode = -1; - } + Console.WriteLine($"Containerize: error CONTAINER001: Failed to push to output registry: {e}"); + Environment.ExitCode = -1; } } } diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs index aaae7486..10993432 100644 --- a/Microsoft.NET.Build.Containers/Image.cs +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -56,7 +56,7 @@ public void AddLayer(Layer l) { newLayers.Add(l); - manifest.layers.Add(new (l.Descriptor.MediaType, l.Descriptor.Size, l.Descriptor.Digest, l.Descriptor.Urls)); + manifest.layers.Add(new(l.Descriptor.MediaType, l.Descriptor.Size, l.Descriptor.Digest, l.Descriptor.Urls)); config["rootfs"]!["diff_ids"]!.AsArray().Add(l.Descriptor.UncompressedDigest); RecalculateDigest(); } @@ -64,7 +64,12 @@ public void AddLayer(Layer l) private void RecalculateDigest() { config["created"] = DateTime.UtcNow; - manifest = manifest with { config = manifest.config with { digest = GetDigest(config), size = Encoding.UTF8.GetBytes(config.ToJsonString()).Length } }; + var newManifestConfig = manifest.config with + { + digest = GetDigest(config), + size = Encoding.UTF8.GetBytes(config.ToJsonString()).Length + }; + manifest.config = newManifestConfig; } private JsonObject CreatePortMap() diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index ea3c406a..0f8729dd 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -27,6 +27,7 @@ public record struct PlatformInformation(string architecture, string os, string? public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform); public record struct ManifestListV2(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests); + public record Registry(Uri BaseUri) { private const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json"; @@ -34,6 +35,14 @@ public record Registry(Uri BaseUri) private const string DockerContainerV1 = "application/vnd.docker.container.image.v1+json"; private const int MaxChunkSizeBytes = 1024 * 64; + public static string[] SupportedRuntimeIdentifiers = new [] { + "linux-x86", + "linux-x64", + "linux-arm", + "linux-arm64", + "win-x64" + }; + private string RegistryName { get; } = BaseUri.Host; public async Task GetImageManifest(string name, string reference, string runtimeIdentifier) @@ -44,7 +53,7 @@ public record Registry(Uri BaseUri) return initialManifestResponse.Content.Headers.ContentType?.MediaType switch { DockerManifestV2 => await TryReadSingleImage(await initialManifestResponse.Content.ReadFromJsonAsync()), DockerManifestListV2 => await TryPickBestImageFromManifestList(await initialManifestResponse.Content.ReadFromJsonAsync(), runtimeIdentifier), - var unknownMediaType => throw new NotImplementedException($"Do not understand the mediaType {unknownMediaType}") + var unknownMediaType => throw new NotImplementedException($"The manifest for {name}:{reference} from registry {BaseUri} was an unknown type: {unknownMediaType}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message.") }; async Task GetManifest(string reference) { @@ -84,7 +93,7 @@ async Task GetBlob(string digest) { ["linux", "arm"] => ("linux", "arm", "v7"), ["linux", "arm64"] => ("linux", "arm64", "v8"), ["win", "x64"] => ("windows", "amd64", null), - var parts => throw new ArgumentException($"Unknown OS/platform combination {String.Join(' ', parts)}") + var parts => throw new ArgumentException($"The runtimeIdentifier '{runtimeIdentifier}' is not supported. The supported RuntimeIdentifiers are {Registry.SupportedRuntimeIdentifiers}.") }; var potentialManifest = manifestList.manifests.SingleOrDefault(manifest => manifest.platform.os == os && manifest.platform.architecture == arch && manifest.platform.variant == variant); @@ -95,7 +104,6 @@ async Task GetBlob(string digest) { return null; } } - } /// From ca47a5c82130a75690e594df9c35983a6e26dc4e Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 1 Dec 2022 12:06:43 -0600 Subject: [PATCH 05/23] WIP support real RID graphs --- Directory.Packages.props | 1 + .../Microsoft.NET.Build.Containers.csproj | 1 + Microsoft.NET.Build.Containers/Registry.cs | 105 +++++++++++++----- .../DockerRegistryManager.cs | 3 +- .../EndToEnd.cs | 7 +- 5 files changed, 84 insertions(+), 33 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 666ec29c..a27b8123 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,5 +16,6 @@ + \ No newline at end of file diff --git a/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj index 820ac071..b200d8d3 100644 --- a/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj +++ b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj @@ -22,6 +22,7 @@ + diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index 0f8729dd..bbdcfcae 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -1,5 +1,6 @@ -using Microsoft.VisualBasic; +using NuGet.Packaging; +using NuGet.RuntimeModel; using System.Diagnostics; using System.IO.Compression; using System.Net; @@ -23,7 +24,7 @@ public record struct ManifestV2(int schemaVersion, string tag, string mediaType, // public enum GoOS { linux, windows }; // not a complete list, only the subset that we support // public enum GoArch { amd64, arm , arm64, [JsonStringEnumMember("386")] x386 }; -public record struct PlatformInformation(string architecture, string os, string? variant, string[] features); +public record struct PlatformInformation(string architecture, string os, string? variant, string[] features, [property:JsonPropertyName("os.version")][field: JsonPropertyName("os.version")] string? version); public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform); public record struct ManifestListV2(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests); @@ -35,14 +36,6 @@ public record Registry(Uri BaseUri) private const string DockerContainerV1 = "application/vnd.docker.container.image.v1+json"; private const int MaxChunkSizeBytes = 1024 * 64; - public static string[] SupportedRuntimeIdentifiers = new [] { - "linux-x86", - "linux-x64", - "linux-arm", - "linux-arm64", - "win-x64" - }; - private string RegistryName { get; } = BaseUri.Host; public async Task GetImageManifest(string name, string reference, string runtimeIdentifier) @@ -83,29 +76,83 @@ async Task GetBlob(string digest) { } async Task TryPickBestImageFromManifestList(ManifestListV2 manifestList, string runtimeIdentifier) { - // TODO: we probably need to pull in actual RID parsing code and look for 'platform' here. - // 'win' can take a version number and we'd break. - // Also, there are more specific linux RIDs (like rhel) that we should instead be looking for the correct 'family' for? - // we probably also need to look at the 'variant' field if the RID contains a version. - (string os, string arch, string? variant) = runtimeIdentifier.Split('-') switch { - ["linux", "x64"] => ("linux", "amd64", null), - ["linux", "x86"] => ("linux", "386", null), - ["linux", "arm"] => ("linux", "arm", "v7"), - ["linux", "arm64"] => ("linux", "arm64", "v8"), - ["win", "x64"] => ("windows", "amd64", null), - var parts => throw new ArgumentException($"The runtimeIdentifier '{runtimeIdentifier}' is not supported. The supported RuntimeIdentifiers are {Registry.SupportedRuntimeIdentifiers}.") - }; - - var potentialManifest = manifestList.manifests.SingleOrDefault(manifest => manifest.platform.os == os && manifest.platform.architecture == arch && manifest.platform.variant == variant); - if (potentialManifest != default) { - var manifestResponse = await GetManifest(potentialManifest.digest); - return await TryReadSingleImage(await manifestResponse.Content.ReadFromJsonAsync()); - } else { - return null; + var runtimeGraph = GetRuntimeGraphForDotNet(); + var compatibleRuntimesForUserRid = runtimeGraph.ExpandRuntime(runtimeIdentifier); + var (ridDict, graphForManifestList) = ConstructRuntimeGraphForManifestList(manifestList, runtimeGraph); + var bestManifestRid = PickBestRidFromCompatibleUserRids(graphForManifestList, compatibleRuntimesForUserRid); + if (bestManifestRid is null) { + throw new ArgumentException($"The runtimeIdentifier '{runtimeIdentifier}' is not supported. The supported RuntimeIdentifiers for the base image {name}:{reference} are {String.Join(",", compatibleRuntimesForUserRid)}"); } + var matchingManifest = ridDict[bestManifestRid]; + var manifestResponse = await GetManifest(matchingManifest.digest); + return await TryReadSingleImage(await manifestResponse.Content.ReadFromJsonAsync()); } } + private string? PickBestRidFromCompatibleUserRids(RuntimeGraph graphForManifestList, IEnumerable compatibleRuntimesForUserRid) + { + return compatibleRuntimesForUserRid.First(rid => graphForManifestList.Runtimes.ContainsKey(rid)); + } + + private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList, RuntimeGraph dotnetRuntimeGraph) + { + var ridDict = new Dictionary(); + var runtimeDescriptionSet = new HashSet(); + foreach (var manifest in manifestList.manifests) { + if (CreateRidForPlatform(manifest.platform) is { } rid) + { + if (ridDict.TryAdd(rid, manifest)) + { + AddRidAndDescendentsToSet(runtimeDescriptionSet, rid, dotnetRuntimeGraph); + } + } + } + + // todo: inheritance relationships for these RIDs? + var graph = new RuntimeGraph(runtimeDescriptionSet); + return (ridDict, graph); + } + + private void AddRidAndDescendentsToSet(HashSet runtimeDescriptionSet, string rid, RuntimeGraph dotnetRuntimeGraph) + { + var R = dotnetRuntimeGraph.Runtimes[rid]; + runtimeDescriptionSet.Add(R); + foreach (var r in R.InheritedRuntimes) AddRidAndDescendentsToSet(runtimeDescriptionSet, r, dotnetRuntimeGraph); + } + + private string? CreateRidForPlatform(PlatformInformation platform) + { + var osPart = platform.os switch + { + "linux" => "linux", + "windows" => "win", + _ => null + }; + // TODO: this part needs a lot of work, the RID graph isn't super precise here and version numbers (especially on windows) are _whack_ + var versionPart = platform.version?.Split('.') switch + { + [var major, .. ] => major, + _ => null + }; + var platformPart = platform.architecture switch + { + "amd64" => "x64", + "x386" => "x86", + "arm" => $"arm{(platform.variant != "v7" ? platform.variant : "")}", + "arm64" => "arm64" + }; + + + if (osPart is null || platformPart is null) return null; + return $"{osPart}{versionPart ?? ""}-{platformPart}"; + } + + private RuntimeGraph GetRuntimeGraphForDotNet() + { + var runtimePath = Path.Combine("C:", "Program Files","dotnet", "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); // how to get this at runtime? + return JsonRuntimeFormat.ReadRuntimeGraph(runtimePath); + } + /// /// Ensure a blob associated with from the registry is available locally. /// diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs index 5178e6a4..bca02b42 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/DockerRegistryManager.cs @@ -54,7 +54,8 @@ public static void StartAndPopulateDockerRegistry(TestContext context) s_registryContainerId = registryContainerId; - foreach (var tag in new [] { Net6ImageTag, Net7ImageTag}) { + foreach (var tag in new[] { Net6ImageTag, Net7ImageTag }) + { Exec("docker", $"pull {BaseImageSource}{BaseImage}:{tag}"); Exec("docker", $"tag {BaseImageSource}{BaseImage}:{tag} {LocalRegistry}/{BaseImage}:{tag}"); Exec("docker", $"push {LocalRegistry}/{BaseImage}:{tag}"); diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index 02b06f99..20fa965b 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -276,11 +276,12 @@ public async Task EndToEnd_NoAPI() privateNuGetAssets.Delete(true); } - [DataRow("linux-x86", false, "/app")] // packaging framework-dependent because missing runtime packs for x86 linux. - [DataRow("linux-x64", true, "/app")] [DataRow("linux-arm", false, "/app")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work + //[DataRow("linux-x86", false, "/app")] // packaging framework-dependent because missing runtime packs for x86 linux. // MS doesn't ship a linux-x86 image [DataRow("linux-arm64", false, "/app")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work [DataRow("win-x64", true, "C:\\app")] + [DataRow("linux-x64", true, "/app")] + [DataRow("debian.11-x64", true, "/app")] // user-reported RID. proves dependency relationships [TestMethod] public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDSpecific, string workingDir) { if (rid == "win-x64") { @@ -290,7 +291,7 @@ public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDS string publishDirectory = await BuildLocalApp(tfm : "net7.0", rid : (isRIDSpecific ? rid : null)); // Build the image - Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); + Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.BaseImageSource)); Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag, rid); From 021c629991a1a063e6c678ee8070a25307581299 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 20 Dec 2022 15:15:04 -0600 Subject: [PATCH 06/23] remove full dependency tracking and just use exact-matching --- Microsoft.NET.Build.Containers/Registry.cs | 70 ++++++++----------- .../EndToEnd.cs | 1 - 2 files changed, 28 insertions(+), 43 deletions(-) diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index bbdcfcae..ed8b1bf9 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -49,20 +49,6 @@ public record Registry(Uri BaseUri) var unknownMediaType => throw new NotImplementedException($"The manifest for {name}:{reference} from registry {BaseUri} was an unknown type: {unknownMediaType}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message.") }; - async Task GetManifest(string reference) { - var client = GetClient(); - var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{reference}")); - response.EnsureSuccessStatusCode(); - return response; - } - - async Task GetBlob(string digest) { - var client = GetClient(); - var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/blobs/{digest}")); - response.EnsureSuccessStatusCode(); - return response; - } - async Task TryReadSingleImage(ManifestV2 manifest) { var config = manifest.config; string configSha = config.digest; @@ -76,50 +62,55 @@ async Task GetBlob(string digest) { } async Task TryPickBestImageFromManifestList(ManifestListV2 manifestList, string runtimeIdentifier) { - var runtimeGraph = GetRuntimeGraphForDotNet(); - var compatibleRuntimesForUserRid = runtimeGraph.ExpandRuntime(runtimeIdentifier); - var (ridDict, graphForManifestList) = ConstructRuntimeGraphForManifestList(manifestList, runtimeGraph); - var bestManifestRid = PickBestRidFromCompatibleUserRids(graphForManifestList, compatibleRuntimesForUserRid); + var (ridDict, graphForManifestList) = ConstructRuntimeGraphForManifestList(manifestList); + var bestManifestRid = CheckIfRidExistsInGraph(graphForManifestList, runtimeIdentifier); if (bestManifestRid is null) { - throw new ArgumentException($"The runtimeIdentifier '{runtimeIdentifier}' is not supported. The supported RuntimeIdentifiers for the base image {name}:{reference} are {String.Join(",", compatibleRuntimesForUserRid)}"); + throw new ArgumentException($"The runtimeIdentifier '{runtimeIdentifier}' is not supported. The supported RuntimeIdentifiers for the base image {name}:{reference} are {String.Join(",", graphForManifestList.Runtimes.Keys)}"); } var matchingManifest = ridDict[bestManifestRid]; var manifestResponse = await GetManifest(matchingManifest.digest); return await TryReadSingleImage(await manifestResponse.Content.ReadFromJsonAsync()); } + + async Task GetManifest(string reference) + { + var client = GetClient(); + var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{reference}")); + response.EnsureSuccessStatusCode(); + return response; + } + + async Task GetBlob(string digest) + { + var client = GetClient(); + var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/blobs/{digest}")); + response.EnsureSuccessStatusCode(); + return response; + } } - private string? PickBestRidFromCompatibleUserRids(RuntimeGraph graphForManifestList, IEnumerable compatibleRuntimesForUserRid) + private string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, string userRid) { - return compatibleRuntimesForUserRid.First(rid => graphForManifestList.Runtimes.ContainsKey(rid)); + graphForManifestList.Runtimes.TryGetValue(userRid, out var runtimeInfo); + return runtimeInfo?.RuntimeIdentifier; } - private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList, RuntimeGraph dotnetRuntimeGraph) + private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList) { var ridDict = new Dictionary(); var runtimeDescriptionSet = new HashSet(); foreach (var manifest in manifestList.manifests) { if (CreateRidForPlatform(manifest.platform) is { } rid) { - if (ridDict.TryAdd(rid, manifest)) - { - AddRidAndDescendentsToSet(runtimeDescriptionSet, rid, dotnetRuntimeGraph); - } + ridDict.TryAdd(rid, manifest); + runtimeDescriptionSet.Add(new RuntimeDescription(rid)); } } - // todo: inheritance relationships for these RIDs? var graph = new RuntimeGraph(runtimeDescriptionSet); return (ridDict, graph); } - private void AddRidAndDescendentsToSet(HashSet runtimeDescriptionSet, string rid, RuntimeGraph dotnetRuntimeGraph) - { - var R = dotnetRuntimeGraph.Runtimes[rid]; - runtimeDescriptionSet.Add(R); - foreach (var r in R.InheritedRuntimes) AddRidAndDescendentsToSet(runtimeDescriptionSet, r, dotnetRuntimeGraph); - } - private string? CreateRidForPlatform(PlatformInformation platform) { var osPart = platform.os switch @@ -129,6 +120,7 @@ private void AddRidAndDescendentsToSet(HashSet runtimeDescri _ => null }; // TODO: this part needs a lot of work, the RID graph isn't super precise here and version numbers (especially on windows) are _whack_ + // TODO: we _may_ need OS-specific version parsing. Need to do more research on what the field looks like across more manifest lists. var versionPart = platform.version?.Split('.') switch { [var major, .. ] => major, @@ -139,20 +131,14 @@ private void AddRidAndDescendentsToSet(HashSet runtimeDescri "amd64" => "x64", "x386" => "x86", "arm" => $"arm{(platform.variant != "v7" ? platform.variant : "")}", - "arm64" => "arm64" + "arm64" => "arm64", + _ => null }; - if (osPart is null || platformPart is null) return null; return $"{osPart}{versionPart ?? ""}-{platformPart}"; } - private RuntimeGraph GetRuntimeGraphForDotNet() - { - var runtimePath = Path.Combine("C:", "Program Files","dotnet", "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); // how to get this at runtime? - return JsonRuntimeFormat.ReadRuntimeGraph(runtimePath); - } - /// /// Ensure a blob associated with from the registry is available locally. /// diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index 20fa965b..a28fa10d 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -281,7 +281,6 @@ public async Task EndToEnd_NoAPI() [DataRow("linux-arm64", false, "/app")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work [DataRow("win-x64", true, "C:\\app")] [DataRow("linux-x64", true, "/app")] - [DataRow("debian.11-x64", true, "/app")] // user-reported RID. proves dependency relationships [TestMethod] public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDSpecific, string workingDir) { if (rid == "win-x64") { From 3510ea2b97580237d0021a5d8a4b6f0fdf6338cf Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 20 Dec 2022 15:28:11 -0600 Subject: [PATCH 07/23] supply platform tags to allow non-native-architecture-images to run during tests --- .../EndToEnd.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index a28fa10d..a473d1ca 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -111,7 +111,7 @@ private static async Task BuildLocalApp(string tfm = "net6.0", string ri await dotnetNew.WaitForExitAsync(); Assert.AreEqual(0, dotnetNew.ExitCode, await dotnetNew.StandardOutput.ReadToEndAsync() + Environment.NewLine + await dotnetNew.StandardError.ReadToEndAsync()); - ProcessStartInfo publishPSI = rid is null ? new("dotnet", $"publish -bl MinimalTestApp") : new("dotnet", $"publish -bl MinimalTestApp -r {rid}"); + ProcessStartInfo publishPSI = rid is null ? new("dotnet", $"publish -bl MinimalTestApp") : new("dotnet", $"publish -bl MinimalTestApp -r {rid} --self-contained"); publishPSI.RedirectStandardOutput = true; publishPSI.RedirectStandardError = true; Process publish = Process.Start(publishPSI); @@ -276,13 +276,13 @@ public async Task EndToEnd_NoAPI() privateNuGetAssets.Delete(true); } - [DataRow("linux-arm", false, "/app")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work - //[DataRow("linux-x86", false, "/app")] // packaging framework-dependent because missing runtime packs for x86 linux. // MS doesn't ship a linux-x86 image - [DataRow("linux-arm64", false, "/app")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work - [DataRow("win-x64", true, "C:\\app")] - [DataRow("linux-x64", true, "/app")] + [DataRow("linux-arm", false, "/app", "linux/arm/v7")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work + //[DataRow("linux-x86", false, "/app", "linux/386")] // packaging framework-dependent because missing runtime packs for x86 linux. // MS doesn't ship a linux-x86 image + [DataRow("linux-arm64", false, "/app", "linux/arm/v8")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work + [DataRow("win-x64", true, "C:\\app", "windows/amd64")] // packaging self-contained because Windows containers can't run on Linux hosts + [DataRow("linux-x64", true, "/app", "linux/amd64")] [TestMethod] - public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDSpecific, string workingDir) { + public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDSpecific, string workingDir, string dockerPlatform) { if (rid == "win-x64") { Assert.Inconclusive("Cannot run Windows containers on Linux hosts (or at the same time as Linux containers), so skipping for now"); return; @@ -306,9 +306,9 @@ public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDS await LocalDocker.Load(x, NewImageName(), rid, DockerRegistryManager.BaseImage); + var args = $"run --rm --tty --platform {dockerPlatform} {NewImageName()}:{rid}"; // Run the image - - ProcessStartInfo runInfo = new("docker", $"run --rm --tty {NewImageName()}:{rid}") { + ProcessStartInfo runInfo = new("docker", args) { RedirectStandardError = true, RedirectStandardOutput = true, }; @@ -316,7 +316,7 @@ public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDS Assert.IsNotNull(run); await run.WaitForExitAsync(); - Assert.AreEqual(0, run.ExitCode, run.StandardOutput.ReadToEnd() + Environment.NewLine + run.StandardError.ReadToEnd()); + Assert.AreEqual(0, run.ExitCode, $"Arguments: {args}\n{run.StandardOutput.ReadToEnd()}\n{run.StandardError.ReadToEnd()}"); string[] DecideEntrypoint(string rid, bool isRIDSpecific, string appName, string workingDir) { var binary = rid.StartsWith("win") ? $"{appName}.exe" : appName; From 999612f92ac87933748d422e9ed0769ee971a23f Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 20 Dec 2022 15:56:09 -0600 Subject: [PATCH 08/23] use correct docker platform --- .../CreateNewImageTests.cs | 2 +- Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs index 5419bd9f..20f58f8e 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs @@ -34,7 +34,7 @@ public void CreateNewImage_Baseline() dotnetNew.WaitForExit(); Assert.AreEqual(0, dotnetNew.ExitCode); - info.Arguments = "publish -c Release -r linux-arm64"; + info.Arguments = "publish -c Release -r linux-arm64 --no-self-contained"; Process dotnetPublish = Process.Start(info); Assert.IsNotNull(dotnetPublish); diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index a473d1ca..dbdb4eb3 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -278,7 +278,7 @@ public async Task EndToEnd_NoAPI() [DataRow("linux-arm", false, "/app", "linux/arm/v7")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work //[DataRow("linux-x86", false, "/app", "linux/386")] // packaging framework-dependent because missing runtime packs for x86 linux. // MS doesn't ship a linux-x86 image - [DataRow("linux-arm64", false, "/app", "linux/arm/v8")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work + [DataRow("linux-arm64", false, "/app", "linux/arm64/v8")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work [DataRow("win-x64", true, "C:\\app", "windows/amd64")] // packaging self-contained because Windows containers can't run on Linux hosts [DataRow("linux-x64", true, "/app", "linux/amd64")] [TestMethod] From fc072a6f3cee6a84572c0360d199d5727f1257eb Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 20 Dec 2022 15:59:27 -0600 Subject: [PATCH 09/23] debug actions build's docker context --- .github/workflows/dotnet.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 1c6fc411..3ec63322 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -26,5 +26,7 @@ jobs: global-json-file: 'global.json' # in addition to the 6.0 we need for tests, install "latest" - name: Build run: dotnet build --consoleloggerparameters:NoSummary --warnAsError + - name: Discover docker platforms + run: docker buildx ls - name: Test run: dotnet test --no-build --logger GitHubActions From 4743318ab5597a743f23ca2ddb4553d818f98cca Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 20 Dec 2022 16:05:07 -0600 Subject: [PATCH 10/23] try to add more platforms to the buildx container --- .github/workflows/dotnet.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3ec63322..80cd4b69 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -26,6 +26,10 @@ jobs: global-json-file: 'global.json' # in addition to the 6.0 we need for tests, install "latest" - name: Build run: dotnet build --consoleloggerparameters:NoSummary --warnAsError + - name: Set up Docker Buildx to handle more platforms + uses: docker/setup-buildx-action@v2 + with: + platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6 - name: Discover docker platforms run: docker buildx ls - name: Test From a00a519691bab1bf089f9e56405ca5fea65b1720 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 20 Dec 2022 16:45:54 -0600 Subject: [PATCH 11/23] document the sadness that is cross-platform container execution --- .github/workflows/dotnet.yml | 6 ----- .../EndToEnd.cs | 24 ++++++++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 80cd4b69..1c6fc411 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -26,11 +26,5 @@ jobs: global-json-file: 'global.json' # in addition to the 6.0 we need for tests, install "latest" - name: Build run: dotnet build --consoleloggerparameters:NoSummary --warnAsError - - name: Set up Docker Buildx to handle more platforms - uses: docker/setup-buildx-action@v2 - with: - platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6 - - name: Discover docker platforms - run: docker buildx ls - name: Test run: dotnet test --no-build --logger GitHubActions diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index dbdb4eb3..0e7ae881 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -276,17 +276,23 @@ public async Task EndToEnd_NoAPI() privateNuGetAssets.Delete(true); } - [DataRow("linux-arm", false, "/app", "linux/arm/v7")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work - //[DataRow("linux-x86", false, "/app", "linux/386")] // packaging framework-dependent because missing runtime packs for x86 linux. // MS doesn't ship a linux-x86 image - [DataRow("linux-arm64", false, "/app", "linux/arm64/v8")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work - [DataRow("win-x64", true, "C:\\app", "windows/amd64")] // packaging self-contained because Windows containers can't run on Linux hosts + // These two are commented because the Github Actions runers don't let us easily configure the Docker Buildx config - + // we need to configure it to allow emulation of other platforms on amd64 hosts before these two will run. + // They do run locally, however. + + //[DataRowAttribute("linux-arm", false, "/app", "linux/arm/v7")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work + //[DataRowAttribute("linux-arm64", false, "/app", "linux/arm64/v8")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work + + // this one should be skipped in all cases because we don't ship linux-x86 runtime packs, so we can't execute the 'apphost' version of the app + //[DataRowAttribute("linux-x86", false, "/app", "linux/386")] // packaging framework-dependent because missing runtime packs for x86 linux. + + // This one should be skipped because containers can't be configured to run on Linux hosts :( + //[DataRow("win-x64", true, "C:\\app", "windows/amd64")] + + // As a result, we only have one actual data-driven test [DataRow("linux-x64", true, "/app", "linux/amd64")] - [TestMethod] + [DataTestMethod] public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDSpecific, string workingDir, string dockerPlatform) { - if (rid == "win-x64") { - Assert.Inconclusive("Cannot run Windows containers on Linux hosts (or at the same time as Linux containers), so skipping for now"); - return; - } string publishDirectory = await BuildLocalApp(tfm : "net7.0", rid : (isRIDSpecific ? rid : null)); // Build the image From 08c09df34914ee3fe42df94f21efe7a1d160e32c Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 21 Dec 2022 12:54:15 -0600 Subject: [PATCH 12/23] remove artificial block for linux-x64 --- .../Microsoft.NET.Build.Containers.targets | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packaging/build/Microsoft.NET.Build.Containers.targets b/packaging/build/Microsoft.NET.Build.Containers.targets index 796a69d5..46d4d9e9 100644 --- a/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/packaging/build/Microsoft.NET.Build.Containers.targets @@ -23,13 +23,6 @@ - - - - - @@ -51,6 +44,8 @@ $(RegistryUrl) $(PublishImageTag) + + $([System.DateTime]::UtcNow.ToString('yyyyMMddhhmmss')) @@ -64,9 +59,11 @@ $(AssemblyName) $(Version) - $([System.DateTime]::UtcNow.ToString('yyyyMMddhhmmss')) /app - + + $(RuntimeIdentifier) + $(NETCoreSdkPortableRuntimeIdentifier) @@ -77,6 +74,7 @@ + @@ -108,7 +106,6 @@ _ContainerVerifySDKVersion; - _ContainerVerifyRuntime; ComputeContainerConfig @@ -137,7 +134,7 @@ Labels="@(ContainerLabel)" ExposedPorts="@(ContainerPort)" ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)" - ContainerRuntimeIdentifier="$(RuntimeIdentifier)"> + ContainerRuntimeIdentifier="$(ContainerRuntimeIdentifier)"> From da69e11c40354194f057f940526efed66a3469f4 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 21 Dec 2022 13:51:14 -0600 Subject: [PATCH 13/23] re-add runtime inheritance for manifest list runtimes --- .../ContainerBuilder.cs | 4 +-- .../ContentStore.cs | 2 ++ .../CreateNewImage.Interface.cs | 7 +++++ .../CreateNewImage.cs | 3 ++- .../CreateNewImageToolTask.cs | 3 ++- Microsoft.NET.Build.Containers/Registry.cs | 27 ++++++++++++------- .../EndToEnd.cs | 10 ++++--- .../RegistryTests.cs | 5 +++- containerize/Program.cs | 7 +++-- .../Microsoft.NET.Build.Containers.targets | 5 ++-- 10 files changed, 51 insertions(+), 22 deletions(-) diff --git a/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/Microsoft.NET.Build.Containers/ContainerBuilder.cs index 488f5607..381ffb58 100644 --- a/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -7,7 +7,7 @@ namespace Microsoft.NET.Build.Containers; public static class ContainerBuilder { - public static async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string[] entrypointArgs, string imageName, string[] imageTags, string outputRegistry, string[] labels, Port[] exposedPorts, string[] envVars, string containerRuntimeIdentifier) + public static async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string[] entrypointArgs, string imageName, string[] imageTags, string outputRegistry, string[] labels, Port[] exposedPorts, string[] envVars, string containerRuntimeIdentifier, string ridGraphPath) { var isDockerPull = String.IsNullOrEmpty(registryName); if (isDockerPull) { @@ -15,7 +15,7 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s } Registry baseRegistry = new Registry(ContainerHelpers.TryExpandRegistryToUri(registryName)); - var img = await baseRegistry.GetImageManifest(baseName, baseTag, containerRuntimeIdentifier); + var img = await baseRegistry.GetImageManifest(baseName, baseTag, containerRuntimeIdentifier, ridGraphPath); if (img is null) { throw new ArgumentException($"Could not find image {baseName}:{baseTag} in registry {registryName} matching RuntimeIdentifier {containerRuntimeIdentifier}"); } diff --git a/Microsoft.NET.Build.Containers/ContentStore.cs b/Microsoft.NET.Build.Containers/ContentStore.cs index 76771900..7c901d28 100644 --- a/Microsoft.NET.Build.Containers/ContentStore.cs +++ b/Microsoft.NET.Build.Containers/ContentStore.cs @@ -34,9 +34,11 @@ public static string PathForDescriptor(Descriptor descriptor) { "application/vnd.docker.image.rootfs.diff.tar.gzip" or "application/vnd.oci.image.layer.v1.tar+gzip" + or "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" => ".tar.gz", "application/vnd.docker.image.rootfs.diff.tar" or "application/vnd.oci.image.layer.v1.tar" + or ":application/vnd.docker.image.rootfs.foreign.diff.tar" => ".tar", _ => throw new ArgumentException($"Unrecognized mediaType '{descriptor.MediaType}'") }; diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs b/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs index cf366d05..2a394cd7 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.Interface.cs @@ -99,6 +99,11 @@ partial class CreateNewImage [Required] public string ContainerRuntimeIdentifier { get; set; } + /// + /// The path to the runtime identifier graph file. This is used to compute RID compatibility for Image Manifest List entries. + /// + [Required] + public string RuntimeIdentifierGraphPath { get; set; } [Output] public string GeneratedContainerManifest { get; set; } @@ -106,6 +111,7 @@ partial class CreateNewImage [Output] public string GeneratedContainerConfiguration { get; set; } + public CreateNewImage() { ContainerizeDirectory = ""; @@ -125,6 +131,7 @@ public CreateNewImage() ExposedPorts = Array.Empty(); ContainerEnvironmentVariables = Array.Empty(); ContainerRuntimeIdentifier = ""; + RuntimeIdentifierGraphPath = ""; GeneratedContainerConfiguration = ""; GeneratedContainerManifest = ""; diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index 9b305510..abbd3ba3 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -78,7 +78,7 @@ private void SetEnvironmentVariables(Image img, ITaskItem[] envVars) throw new ArgumentException("Don't know how to pull images from local daemons at the moment"); } else { var reg = new Registry(ContainerHelpers.TryExpandRegistryToUri(BaseRegistry)); - return reg.GetImageManifest(BaseImageName, BaseImageTag, ContainerRuntimeIdentifier).Result; + return reg.GetImageManifest(BaseImageName, BaseImageTag, ContainerRuntimeIdentifier, RuntimeIdentifierGraphPath).Result; } } @@ -88,6 +88,7 @@ private void SafeLog(string message, params object[] formatParams) { public override bool Execute() { + System.Diagnostics.Debugger.Launch(); if (!Directory.Exists(PublishDirectory)) { Log.LogError("{0} '{1}' does not exist", nameof(PublishDirectory), PublishDirectory); diff --git a/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs b/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs index b77276ec..95dcccf9 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs @@ -83,7 +83,8 @@ protected override string GenerateCommandLineCommands() (ImageTags.Length > 0 ? " --imagetags " + String.Join(" ", ImageTags.Select((i) => Quote(i))) : "") + (EntrypointArgs.Length > 0 ? " --entrypointargs " + String.Join(" ", EntrypointArgs.Select((i) => i.ItemSpec)) : "") + (ExposedPorts.Length > 0 ? " --ports " + String.Join(" ", ExposedPorts.Select((i) => i.ItemSpec + "/" + i.GetMetadata("Type"))) : "") + - (ContainerEnvironmentVariables.Length > 0 ? " --environmentvariables " + String.Join(" ", ContainerEnvironmentVariables.Select((i) => i.ItemSpec + "=" + Quote(i.GetMetadata("Value")))) : ""); + (ContainerEnvironmentVariables.Length > 0 ? " --environmentvariables " + String.Join(" ", ContainerEnvironmentVariables.Select((i) => i.ItemSpec + "=" + Quote(i.GetMetadata("Value")))) : "") + + $" --ridgraphpath {Quote(RuntimeIdentifierGraphPath)}"; } private string Quote(string path) diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index ed8b1bf9..4279fb09 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -38,7 +38,7 @@ public record Registry(Uri BaseUri) private string RegistryName { get; } = BaseUri.Host; - public async Task GetImageManifest(string name, string reference, string runtimeIdentifier) + public async Task GetImageManifest(string name, string reference, string runtimeIdentifier, string runtimeIdentifierGraphPath) { var client = GetClient(); var initialManifestResponse = await GetManifest(reference); @@ -62,7 +62,8 @@ public record Registry(Uri BaseUri) } async Task TryPickBestImageFromManifestList(ManifestListV2 manifestList, string runtimeIdentifier) { - var (ridDict, graphForManifestList) = ConstructRuntimeGraphForManifestList(manifestList); + var runtimeGraph = GetRuntimeGraphForDotNet(runtimeIdentifierGraphPath); + var (ridDict, graphForManifestList) = ConstructRuntimeGraphForManifestList(manifestList, runtimeGraph); var bestManifestRid = CheckIfRidExistsInGraph(graphForManifestList, runtimeIdentifier); if (bestManifestRid is null) { throw new ArgumentException($"The runtimeIdentifier '{runtimeIdentifier}' is not supported. The supported RuntimeIdentifiers for the base image {name}:{reference} are {String.Join(",", graphForManifestList.Runtimes.Keys)}"); @@ -89,21 +90,18 @@ async Task GetBlob(string digest) } } - private string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, string userRid) - { - graphForManifestList.Runtimes.TryGetValue(userRid, out var runtimeInfo); - return runtimeInfo?.RuntimeIdentifier; - } + private string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, string userRid) => graphForManifestList.Runtimes.FirstOrDefault(kvp => graphForManifestList.AreCompatible(kvp.Key, userRid)).Key; - private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList) + private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList, RuntimeGraph dotnetRuntimeGraph) { var ridDict = new Dictionary(); var runtimeDescriptionSet = new HashSet(); foreach (var manifest in manifestList.manifests) { if (CreateRidForPlatform(manifest.platform) is { } rid) { - ridDict.TryAdd(rid, manifest); - runtimeDescriptionSet.Add(new RuntimeDescription(rid)); + if (ridDict.TryAdd(rid, manifest)) { + AddRidAndDescendantsToSet(runtimeDescriptionSet, rid, dotnetRuntimeGraph); + } } } @@ -139,6 +137,15 @@ async Task GetBlob(string digest) return $"{osPart}{versionPart ?? ""}-{platformPart}"; } + private RuntimeGraph GetRuntimeGraphForDotNet(string ridGraphPath) => JsonRuntimeFormat.ReadRuntimeGraph(ridGraphPath); + + private void AddRidAndDescendantsToSet(HashSet runtimeDescriptionSet, string rid, RuntimeGraph dotnetRuntimeGraph) + { + var R = dotnetRuntimeGraph.Runtimes[rid]; + runtimeDescriptionSet.Add(R); + foreach (var r in R.InheritedRuntimes) AddRidAndDescendantsToSet(runtimeDescriptionSet, r, dotnetRuntimeGraph); + } + /// /// Ensure a blob associated with from the registry is available locally. /// diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index 0e7ae881..3a1b521f 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -10,6 +10,10 @@ namespace Test.Microsoft.NET.Build.Containers.Filesystem; [TestClass] public class EndToEnd { + public static string RuntimeGraphFilePath() => + // TODO: The DOTNET_ROOT comes from the test host, but we have no idea what the SDK version is. + Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); + public static string NewImageName([CallerMemberName] string callerMemberName = "") { bool normalized = ContainerHelpers.NormalizeImageName(callerMemberName, out string normalizedName); @@ -31,7 +35,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64"); + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", RuntimeGraphFilePath()); Layer l = Layer.FromDirectory(publishDirectory, "/app"); @@ -69,7 +73,7 @@ public async Task ApiEndToEndWithLocalLoad() Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64"); + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", RuntimeGraphFilePath()); Layer l = Layer.FromDirectory(publishDirectory, "/app"); @@ -298,7 +302,7 @@ 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); + Image x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag, rid, RuntimeGraphFilePath()); Layer l = Layer.FromDirectory(publishDirectory, "/app"); diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs index 66498a6d..20ebd293 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs @@ -15,7 +15,10 @@ public async Task GetFromRegistry() { Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry)); - Image downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64"); + // TODO: The DOTNET_ROOT comes from the test host, but we have no idea what the SDK version is. + var ridgraphfile = Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); + + Image downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", ridgraphfile); // don't need rid graph for local registry Assert.IsNotNull(downloadedImage); } diff --git a/containerize/Program.cs b/containerize/Program.cs index 71a9de35..3e775c6c 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -153,6 +153,7 @@ var ridOpt = new Option(name: "--rid", description: "Runtime Identifier of the generated container."); +var ridGraphPathOpt = new Option(name: "--ridgraphpath", description: "Path to the RID graph file."); RootCommand root = new RootCommand("Containerize an application without Docker.") { @@ -169,7 +170,8 @@ labelsOpt, portsOpt, envVarsOpt, - ridOpt + ridOpt, + ridGraphPathOpt }; root.SetHandler(async (context) => @@ -188,7 +190,8 @@ Port[] _ports = context.ParseResult.GetValueForOption(portsOpt) ?? Array.Empty(); string[] _envVars = context.ParseResult.GetValueForOption(envVarsOpt) ?? Array.Empty(); string _rid = context.ParseResult.GetValueForOption(ridOpt) ?? ""; - await ContainerBuilder.Containerize(_publishDir, _workingDir, _baseReg, _baseName, _baseTag, _entrypoint, _entrypointArgs, _name, _tags, _outputReg, _labels, _ports, _envVars, _rid); + string _ridGraphPath = context.ParseResult.GetValueForOption(ridGraphPathOpt) ?? ""; + await ContainerBuilder.Containerize(_publishDir, _workingDir, _baseReg, _baseName, _baseTag, _entrypoint, _entrypointArgs, _name, _tags, _outputReg, _labels, _ports, _envVars, _rid, _ridGraphPath); }); return await root.InvokeAsync(args); diff --git a/packaging/build/Microsoft.NET.Build.Containers.targets b/packaging/build/Microsoft.NET.Build.Containers.targets index 46d4d9e9..ae838494 100644 --- a/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/packaging/build/Microsoft.NET.Build.Containers.targets @@ -62,7 +62,7 @@ /app - $(RuntimeIdentifier) + $(RuntimeIdentifier) $(NETCoreSdkPortableRuntimeIdentifier) @@ -134,7 +134,8 @@ Labels="@(ContainerLabel)" ExposedPorts="@(ContainerPort)" ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)" - ContainerRuntimeIdentifier="$(ContainerRuntimeIdentifier)"> + ContainerRuntimeIdentifier="$(ContainerRuntimeIdentifier)" + RuntimeIdentifierGraphPath="$(RuntimeIdentifierGraphPath)"> From aeb099c76404d5bc5576d82d126736a966e6d654 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 21 Dec 2022 14:43:54 -0600 Subject: [PATCH 14/23] fix the things --- Microsoft.NET.Build.Containers/CreateNewImage.cs | 1 - .../CreateNewImageTests.cs | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index abbd3ba3..d21fca76 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -88,7 +88,6 @@ private void SafeLog(string message, params object[] formatParams) { public override bool Execute() { - System.Diagnostics.Debugger.Launch(); if (!Directory.Exists(PublishDirectory)) { Log.LogError("{0} '{1}' does not exist", nameof(PublishDirectory), PublishDirectory); diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs index 20f58f8e..9f5f24b0 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs @@ -9,6 +9,10 @@ namespace Test.Microsoft.NET.Build.Containers.Tasks; [TestClass] public class CreateNewImageTests { + public static string RuntimeGraphFilePath() => + // TODO: The DOTNET_ROOT comes from the test host, but we have no idea what the SDK version is. + Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); + [TestMethod] public void CreateNewImage_Baseline() { @@ -53,6 +57,7 @@ public void CreateNewImage_Baseline() task.WorkingDirectory = "app/"; task.ContainerRuntimeIdentifier = "linux-arm64"; task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") }; + task.RuntimeIdentifierGraphPath = RuntimeGraphFilePath(); Assert.IsTrue(task.Execute()); newProjectDir.Delete(true); @@ -116,6 +121,7 @@ public void ParseContainerProperties_EndToEnd() cni.Entrypoint = new TaskItem[] { new("ParseContainerProperties_EndToEnd") }; cni.ImageTags = pcp.NewContainerTags; cni.ContainerRuntimeIdentifier = "linux-x64"; + cni.RuntimeIdentifierGraphPath = RuntimeGraphFilePath(); Assert.IsTrue(cni.Execute()); newProjectDir.Delete(true); @@ -193,6 +199,7 @@ public void Tasks_EndToEnd_With_EnvironmentVariable_Validation() cni.ImageTags = pcp.NewContainerTags; cni.ContainerEnvironmentVariables = pcp.NewContainerEnvironmentVariables; cni.ContainerRuntimeIdentifier = "linux-x64"; + cni.RuntimeIdentifierGraphPath = RuntimeGraphFilePath(); Assert.IsTrue(cni.Execute()); From 2ad8afd9c130c1fd3da58763d360c8a46b1be1ac Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 21 Dec 2022 14:56:33 -0600 Subject: [PATCH 15/23] documentation for the new ContainerRuntimeIdentifier property --- docs/ContainerCustomization.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/ContainerCustomization.md b/docs/ContainerCustomization.md index 891e7ee9..9abd92df 100644 --- a/docs/ContainerCustomization.md +++ b/docs/ContainerCustomization.md @@ -24,6 +24,18 @@ If you set a value here, you should set the fully-qualified name of the image to mcr.microsoft.com/dotnet/runtime:6.0 ``` +## ContainerRuntimeIdentifier + +This property controls the OS and platform used by your container if your [`ContainerBaseImage`](#containerbaseimage) is a 'Manifest List'. Manifest Lists are images that support more than one architecture behind a single, common, name. For example, the `mcr.microsoft.com/dotnet/runtime` image is a manifest list that supports `linux-x64`, `linux-arm`, `linux-arm64` and `win10-x64` images. + +When a Manifest List is your base image, we need to choose the most relevant image to use as the base. We do this by choosing the image that best matches the `RuntimeIdentifier` of your project. If you set a value here, we will use that value to choose the best image to use as the base. Valid values for this property will vary based on the image you choose, but will always be in the form of a .NET SDK Runtime Identifier. + +By default, if your project has a RuntimeIdentifier set, that value will be used. A RuntimeIdentifer is usually set via the `-r` parameter to the `dotnet publish` command, or by setting the `RuntimeIdentifier` property in a PublishProfile used from Visual Studio. + +```xml +linux-x64 +``` + ## ContainerRegistry This property controls the destination registry - the place that the newly-created image will be pushed to. From bbf039a08fcf2d6899c2a8ec52acdab32634c9b1 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 19 Jan 2023 10:25:15 -0600 Subject: [PATCH 16/23] Add NuGet assemblies to package --- packaging/package.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/package.csproj b/packaging/package.csproj index f03721dd..f8a3d042 100644 --- a/packaging/package.csproj +++ b/packaging/package.csproj @@ -49,6 +49,8 @@ + + From 18ccb522bd13eb50a50ff810374b22325f51c60d Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 19 Jan 2023 10:33:32 -0600 Subject: [PATCH 17/23] Pass RID through ToolTask implementation --- Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs b/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs index 15abccec..7711b6ac 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs @@ -84,6 +84,7 @@ protected override string GenerateCommandLineCommands() (EntrypointArgs.Length > 0 ? " --entrypointargs " + String.Join(" ", EntrypointArgs.Select((i) => i.ItemSpec)) : "") + (ExposedPorts.Length > 0 ? " --ports " + String.Join(" ", ExposedPorts.Select((i) => i.ItemSpec + "/" + i.GetMetadata("Type"))) : "") + (ContainerEnvironmentVariables.Length > 0 ? " --environmentvariables " + String.Join(" ", ContainerEnvironmentVariables.Select((i) => i.ItemSpec + "=" + Quote(i.GetMetadata("Value")))) : "") + + $" --rid {Quote(ContainerRuntimeIdentifier)}" + $" --ridgraphpath {Quote(RuntimeIdentifierGraphPath)}"; } From 37684cc008f1d41bc578dd3762fc6390f4752d52 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 19 Jan 2023 11:14:21 -0600 Subject: [PATCH 18/23] Pick a rid graph at semi-random for testing We don't do anything too complicated so 'most recent'-ish should be fine. --- .../CreateNewImageTests.cs | 12 ++++++++---- .../EndToEnd.cs | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs index d146ddb3..6ddbcff5 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs @@ -9,10 +9,14 @@ namespace Test.Microsoft.NET.Build.Containers.Tasks; [TestClass] public class CreateNewImageTests { - public static string RuntimeGraphFilePath() => - // TODO: The DOTNET_ROOT comes from the test host, but we have no idea what the SDK version is. - Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); - + public static string RuntimeGraphFilePath() { + DirectoryInfo sdksDir = new(Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk")); + + var lastWrittenSdk = sdksDir.EnumerateDirectories().OrderByDescending(di => di.LastWriteTime).First(); + + return lastWrittenSdk.GetFiles("RuntimeIdentifierGraph.json").Single().FullName; + } + [TestMethod] public void CreateNewImage_Baseline() { diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs index 2220c3f0..0d1e8426 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs @@ -10,9 +10,13 @@ namespace Test.Microsoft.NET.Build.Containers.Filesystem; [TestClass] public class EndToEnd { - public static string RuntimeGraphFilePath() => - // TODO: The DOTNET_ROOT comes from the test host, but we have no idea what the SDK version is. - Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); + public static string RuntimeGraphFilePath() { + DirectoryInfo sdksDir = new(Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk")); + + var lastWrittenSdk = sdksDir.EnumerateDirectories().OrderByDescending(di => di.LastWriteTime).First(); + + return lastWrittenSdk.GetFiles("RuntimeIdentifierGraph.json").Single().FullName; + } public static string NewImageName([CallerMemberName] string callerMemberName = "") { From 53a8b6c4e4c5cbb3d67b01b3bb4301b6b8781f81 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Jan 2023 11:16:03 -0600 Subject: [PATCH 19/23] remove invalid mediaType check --- Microsoft.NET.Build.Containers/ContentStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Microsoft.NET.Build.Containers/ContentStore.cs b/Microsoft.NET.Build.Containers/ContentStore.cs index 7c901d28..3bdc1d42 100644 --- a/Microsoft.NET.Build.Containers/ContentStore.cs +++ b/Microsoft.NET.Build.Containers/ContentStore.cs @@ -38,7 +38,6 @@ public static string PathForDescriptor(Descriptor descriptor) => ".tar.gz", "application/vnd.docker.image.rootfs.diff.tar" or "application/vnd.oci.image.layer.v1.tar" - or ":application/vnd.docker.image.rootfs.foreign.diff.tar" => ".tar", _ => throw new ArgumentException($"Unrecognized mediaType '{descriptor.MediaType}'") }; From 8bbd20151a632c679999a31912d484099be2a448 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Jan 2023 13:36:09 -0600 Subject: [PATCH 20/23] Address some review questions --- Microsoft.NET.Build.Containers/Registry.cs | 9 ++++----- .../RegistryTests.cs | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index 50d9da2c..40dd22d5 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -20,10 +20,6 @@ public record struct ManifestConfig(string mediaType, long size, string digest); public record struct ManifestLayer(string mediaType, long size, string digest, string[]? urls); public record struct ManifestV2(int schemaVersion, string tag, string mediaType, ManifestConfig config, List layers); -// not a complete list, only the subset that we support -// public enum GoOS { linux, windows }; -// not a complete list, only the subset that we support -// public enum GoArch { amd64, arm , arm64, [JsonStringEnumMember("386")] x386 }; public record struct PlatformInformation(string architecture, string os, string? variant, string[] features, [property:JsonPropertyName("os.version")][field: JsonPropertyName("os.version")] string? version); public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform); public record struct ManifestListV2(int schemaVersion, string mediaType, PlatformSpecificManifest[] manifests); @@ -170,7 +166,10 @@ async Task GetBlob(string repositoryName, string digest) } private string? CreateRidForPlatform(PlatformInformation platform) - { + { + // we only support linux and windows containers explicitly, so anything else we should skip past. + // there are theoretically other platforms/architectures that Docker supports (s390x?), but we are + // deliberately ignoring them without clear user signal. var osPart = platform.os switch { "linux" => "linux", diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs index 6debb89c..4105bba4 100644 --- a/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs +++ b/Test.Microsoft.NET.Build.Containers.Filesystem/RegistryTests.cs @@ -18,7 +18,9 @@ public async Task GetFromRegistry() // TODO: The DOTNET_ROOT comes from the test host, but we have no idea what the SDK version is. var ridgraphfile = Path.Combine(Environment.GetEnvironmentVariable("DOTNET_ROOT"), "sdk", "7.0.100", "RuntimeIdentifierGraph.json"); - Image downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", ridgraphfile); // don't need rid graph for local registry + // 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); Assert.IsNotNull(downloadedImage); } From bfba2ac8cd680cbace070e76a4ea04f2cddf61bb Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Jan 2023 16:19:28 -0600 Subject: [PATCH 21/23] only use the LEAF rids to compute compatibility with user-provided RIDs This ensures that valid-but-meaninless in this context RIDs (like unix, any, base, etc.) are not take into consideration for compatibility --- Microsoft.NET.Build.Containers/Registry.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index 40dd22d5..6bb141f6 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -146,7 +146,12 @@ async Task GetBlob(string repositoryName, string digest) return response; } - private string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, string userRid) => graphForManifestList.Runtimes.FirstOrDefault(kvp => graphForManifestList.AreCompatible(kvp.Key, userRid)).Key; + private string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, string userRid) { + var leafRids = + graphForManifestList.Runtimes.Keys + .Where(k => !graphForManifestList.Runtimes.Values.Any(r => r.InheritedRuntimes.Contains(k))); + return leafRids.FirstOrDefault(leaf => graphForManifestList.AreCompatible(leaf, userRid)); + } private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList, RuntimeGraph dotnetRuntimeGraph) { From abd8c18519f42e27ecb0fc61990c08cd6ce77dde Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Jan 2023 17:02:35 -0600 Subject: [PATCH 22/23] bring clarity to the RIDs --- Microsoft.NET.Build.Containers/Registry.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index 6bb141f6..092a03a6 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -121,7 +121,7 @@ public readonly bool IsGoogleArtifactRegistry { async Task TryPickBestImageFromManifestList(string repositoryName, string reference, ManifestListV2 manifestList, string runtimeIdentifier, string runtimeIdentifierGraphPath) { var runtimeGraph = GetRuntimeGraphForDotNet(runtimeIdentifierGraphPath); var (ridDict, graphForManifestList) = ConstructRuntimeGraphForManifestList(manifestList, runtimeGraph); - var bestManifestRid = CheckIfRidExistsInGraph(graphForManifestList, runtimeIdentifier); + var bestManifestRid = CheckIfRidExistsInGraph(graphForManifestList, ridDict.Keys, runtimeIdentifier); if (bestManifestRid is null) { throw new ArgumentException($"The runtimeIdentifier '{runtimeIdentifier}' is not supported. The supported RuntimeIdentifiers for the base image {repositoryName}:{reference} are {String.Join(",", graphForManifestList.Runtimes.Keys)}"); } @@ -146,12 +146,7 @@ async Task GetBlob(string repositoryName, string digest) return response; } - private string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, string userRid) { - var leafRids = - graphForManifestList.Runtimes.Keys - .Where(k => !graphForManifestList.Runtimes.Values.Any(r => r.InheritedRuntimes.Contains(k))); - return leafRids.FirstOrDefault(leaf => graphForManifestList.AreCompatible(leaf, userRid)); - } + private string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, IEnumerable leafRids, string userRid) => leafRids.FirstOrDefault(leaf => graphForManifestList.AreCompatible(leaf, userRid)); private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList, RuntimeGraph dotnetRuntimeGraph) { From 81c97687d534d714cf3f36430660d616f9078c9e Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 19 Jan 2023 17:07:58 -0600 Subject: [PATCH 23/23] remove linux only messaging --- docs/ContainerCustomization.md | 3 --- docs/GettingStarted.md | 2 -- docs/ZeroToContainer.md | 1 - packaging/README.md | 1 - 4 files changed, 7 deletions(-) diff --git a/docs/ContainerCustomization.md b/docs/ContainerCustomization.md index fb15e9ad..7fd9248e 100644 --- a/docs/ContainerCustomization.md +++ b/docs/ContainerCustomization.md @@ -5,9 +5,6 @@ You can control many aspects of the generated container through MSBuild properti > **Note** > The only exception to this is `RUN` commands - due to the way we build containers, those cannot be emulated. If you need this functionality, you will need to use a Dockerfile to build your container images. -> **Note** -> This package only supports Linux containers in this version. - ## ContainerBaseImage This property controls the image used as the basis for your image. By default, we will infer the following values for you based on the properties of your project: diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 9b51219b..3046f9af 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -21,8 +21,6 @@ The `Microsoft.NET.Build.Containers` package infers a number of properties about For more information, see [Customizing a container](./ContainerCustomization.md) -> **Note** -> This package only supports Linux containers in this version. > **Note** > If you are publishing a console application (or any non-Web project) you will need to add the `/t:PublishContainer` option to the command line above. See [dotnet/sdk-container-builds#141](https://github.com/dotnet/sdk-container-builds/issues/141) for more details. \ No newline at end of file diff --git a/docs/ZeroToContainer.md b/docs/ZeroToContainer.md index 2b386d71..087dd7c0 100644 --- a/docs/ZeroToContainer.md +++ b/docs/ZeroToContainer.md @@ -7,7 +7,6 @@ You should expect it to shrink noticeably over time! * [.NET SDK 7.0.100-preview.7](https://dotnet.microsoft.com/download/dotnet/7.0) or higher * Docker should be installed and running -* On Windows, Docker must be [configured for Linux containers](https://docs.microsoft.com/virtualization/windowscontainers/quick-start/quick-start-windows-10-linux) ## Usage diff --git a/packaging/README.md b/packaging/README.md index 7fb9a94b..82ad16da 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -17,6 +17,5 @@ Pushed container ':' to registry 'docker://' Out of the box, this package will infer a number of properties about the generated container image, including which base image to use, which version of that image to use, and where to push the generated image. You have control over all of these properties, however. You can read more about these customizations [here](https://aka.ms/dotnet/containers/customization). -**Note**: This package only supports Linux containers in this version. **Note**: This package only supports Web projects (those that use the `Microsoft.NET.Sdk.Web` SDK) in this version. \ No newline at end of file