|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Formats.Tar;
using System.Runtime.CompilerServices;
using System.Text.Json;
using FakeItEasy;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.NET.Build.Containers.LocalDaemons;
using Microsoft.NET.Build.Containers.Resources;
using Microsoft.NET.Build.Containers.UnitTests;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
[Collection("Docker tests")]
public class EndToEndTests : IDisposable
{
private ITestOutputHelper _testOutput;
private readonly TestLoggerFactory _loggerFactory;
public EndToEndTests(ITestOutputHelper testOutput)
{
_testOutput = testOutput;
_loggerFactory = new TestLoggerFactory(testOutput);
}
public static string NewImageName([CallerMemberName] string callerMemberName = "")
{
var (normalizedName, warning, error) = ContainerHelpers.NormalizeRepository(callerMemberName);
if (error is (var format, var args))
{
throw new ArgumentException(string.Format(Strings.ResourceManager.GetString(format)!, args));
}
return normalizedName!; // non-null if error is null
}
public void Dispose()
{
_loggerFactory.Dispose();
}
internal static readonly string _oldFramework = "net9.0";
// CLI will not let us to target net9.0 anymore but we still need it because images for net10.0 aren't ready yet.
// so we let it create net10.0 app, then change the target. Since we're building just small sample applications, it works.
internal static void ChangeTargetFrameworkAfterAppCreation(string path)
{
DirectoryInfo d = new DirectoryInfo(path);
FileInfo[] Files = d.GetFiles("*.csproj"); //Getting .csproj files
string csprojFilename = Files[0].Name; // There is only one
string text = File.ReadAllText(Path.Combine(path, csprojFilename));
text = text.Replace("net10.0", _oldFramework);
File.WriteAllText(Path.Combine(path, csprojFilename), text);
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async Task ApiEndToEndWithRegistryPushAndPull()
{
ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithRegistryPushAndPull));
string publishDirectory = BuildLocalApp();
// Build the image
Registry registry = new(DockerRegistryManager.LocalRegistry, logger, RegistryMode.Push);
ImageBuilder imageBuilder = await registry.GetImageManifestAsync(
DockerRegistryManager.RuntimeBaseImage,
DockerRegistryManager.Net9ImageTag,
"linux-x64",
ToolsetUtils.RidGraphManifestPicker,
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);
Layer l = Layer.FromDirectory(publishDirectory, "/app", false, imageBuilder.ManifestMediaType);
imageBuilder.AddLayer(l);
imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty<string>());
BuiltImage builtImage = imageBuilder.Build();
// Push the image back to the local registry
var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null);
var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" });
await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false);
foreach (string tag in destinationReference.Tags)
{
// pull it back locally
ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:{tag}")
.Execute()
.Should().Pass();
// Run the image
ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:{tag}")
.Execute()
.Should().Pass();
}
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async Task ApiEndToEndWithLocalLoad()
{
ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithLocalLoad));
string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework);
// Build the image
Registry registry = new(DockerRegistryManager.LocalRegistry, logger, RegistryMode.Push);
ImageBuilder imageBuilder = await registry.GetImageManifestAsync(
DockerRegistryManager.RuntimeBaseImage,
DockerRegistryManager.Net9ImageTag,
"linux-x64",
ToolsetUtils.RidGraphManifestPicker,
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);
Layer l = Layer.FromDirectory(publishDirectory, "/app", false, imageBuilder.ManifestMediaType);
imageBuilder.AddLayer(l);
imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty<string>());
BuiltImage builtImage = imageBuilder.Build();
// Load the image into the local registry
var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null);
var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" });
await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);
// Run the image
foreach (string tag in destinationReference.Tags)
{
ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{NewImageName()}:{tag}")
.Execute()
.Should().Pass();
}
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async Task ApiEndToEndWithArchiveWritingAndLoad()
{
ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithArchiveWritingAndLoad));
string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework);
// Build the image
Registry registry = new(DockerRegistryManager.LocalRegistry, logger, RegistryMode.Push);
ImageBuilder imageBuilder = await registry.GetImageManifestAsync(
DockerRegistryManager.RuntimeBaseImage,
DockerRegistryManager.Net9ImageTag,
"linux-x64",
ToolsetUtils.RidGraphManifestPicker,
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);
Layer l = Layer.FromDirectory(publishDirectory, "/app", false, imageBuilder.ManifestMediaType);
imageBuilder.AddLayer(l);
imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty<string>());
BuiltImage builtImage = imageBuilder.Build();
// Write the image to disk
var archiveFile = Path.Combine(TestSettings.TestArtifactsDirectory,
nameof(ApiEndToEndWithArchiveWritingAndLoad), "app.tar.gz");
var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null);
var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archiveFile), NewImageName(), new[] { "latest", "1.0" });
await destinationReference.LocalRegistry!.LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);
Assert.True(File.Exists(archiveFile), $"File.Exists({archiveFile})");
// Load the archive
ContainerCli.LoadCommand(_testOutput, "--input", archiveFile)
.Execute()
.Should().Pass();
// Run the image
foreach (string tag in destinationReference.Tags)
{
ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{NewImageName()}:{tag}")
.Execute()
.Should().Pass();
}
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async Task TarballsHaveCorrectStructure()
{
var archiveFile = Path.Combine(TestSettings.TestArtifactsDirectory,
nameof(TarballsHaveCorrectStructure), "app.tar.gz");
// 1. Create docker image and write it to a tarball
(BuiltImage dockerImage, SourceImageReference sourceReference, DestinationImageReference destinationReference) =
await BuildDockerImageWithArciveDestinationAsync(archiveFile, ["latest"], nameof(TarballsHaveCorrectStructure));
await destinationReference.LocalRegistry!.LoadAsync(dockerImage, sourceReference, destinationReference, default).ConfigureAwait(false);
Assert.True(File.Exists(archiveFile), $"File.Exists({archiveFile})");
CheckDockerTarballStructure(archiveFile);
// 2. Convert the docker image to an OCI image and write it to a tarball
BuiltImage ociImage = ConvertToOciImage(dockerImage);
await destinationReference.LocalRegistry!.LoadAsync(ociImage, sourceReference, destinationReference, default).ConfigureAwait(false);
Assert.True(File.Exists(archiveFile), $"File.Exists({archiveFile})");
CheckOciTarballStructure(archiveFile);
}
private async Task<(BuiltImage image, SourceImageReference sourceReference, DestinationImageReference destinationReference)> BuildDockerImageWithArciveDestinationAsync(string archiveFile, string[] tags, string testName)
{
ILogger logger = _loggerFactory.CreateLogger(testName);
Registry registry = new(DockerRegistryManager.LocalRegistry, logger, RegistryMode.Push);
ImageBuilder imageBuilder = await registry.GetImageManifestAsync(
DockerRegistryManager.RuntimeBaseImage,
DockerRegistryManager.Net8ImageTag,
"linux-x64",
ToolsetUtils.RidGraphManifestPicker,
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);
BuiltImage builtImage = imageBuilder.Build();
// Write the image to disk
var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net7ImageTag, null);
var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archiveFile), NewImageName(), tags);
return (builtImage, sourceReference, destinationReference);
}
private BuiltImage ConvertToOciImage(BuiltImage builtImage)
{
// Convert the image to an OCI image
var ociImage = new BuiltImage
{
Config = builtImage.Config,
ImageDigest = builtImage.ImageDigest,
ImageSha = builtImage.ImageSha,
Manifest = builtImage.Manifest,
ManifestDigest = builtImage.ManifestDigest,
ManifestMediaType = SchemaTypes.OciManifestV1,
Layers = builtImage.Layers
};
return ociImage;
}
private void CheckDockerTarballStructure(string tarball)
{
var layersCount = 0;
int configJson = 0;
int manifestJsonCount = 0;
using (FileStream fs = new FileStream(tarball, FileMode.Open, FileAccess.Read))
using (var tarReader = new TarReader(fs))
{
var entry = tarReader.GetNextEntry();
while (entry is not null)
{
if (entry.Name == "manifest.json")
{
manifestJsonCount++;
}
else if (entry.Name.EndsWith(".json"))
{
configJson++;
}
else if (entry.Name.EndsWith("/layer.tar"))
{
layersCount++;
}
else
{
Assert.Fail($"Unexpected entry in tarball: {entry.Name}");
}
entry = tarReader.GetNextEntry();
}
}
Assert.Equal(1, manifestJsonCount);
Assert.Equal(1, configJson);
Assert.True(layersCount > 0);
}
private void CheckOciTarballStructure(string tarball)
{
int blobsCount = 0;
int ociLayoutCount = 0;
int indexJsonCount = 0;
using (FileStream fs = new FileStream(tarball, FileMode.Open, FileAccess.Read))
using (var tarReader = new TarReader(fs))
{
var entry = tarReader.GetNextEntry();
while (entry is not null)
{
if (entry.Name == "oci-layout")
{
ociLayoutCount++;
}
else if (entry.Name == "index.json")
{
indexJsonCount++;
}
else if (entry.Name.StartsWith("blobs/sha256/"))
{
blobsCount++;
}
else
{
Assert.Fail($"Unexpected entry in tarball: {entry.Name}");
}
entry = tarReader.GetNextEntry();
}
}
Assert.Equal(1, ociLayoutCount);
Assert.Equal(1, indexJsonCount);
Assert.True(blobsCount > 0);
}
private string BuildLocalApp([CallerMemberName] string testName = "TestName", string tfm = ToolsetInfo.CurrentTargetFramework, string rid = "linux-x64")
{
string workingDirectory = Path.Combine(TestSettings.TestArtifactsDirectory, testName);
DirectoryInfo d = new(Path.Combine(workingDirectory, "MinimalTestApp"));
if (d.Exists)
{
d.Delete(recursive: true);
}
Directory.CreateDirectory(workingDirectory);
new DotnetNewCommand(_testOutput, "console", "-f", tfm, "-o", "MinimalTestApp")
.WithVirtualHive()
.WithWorkingDirectory(workingDirectory)
.Execute()
.Should().Pass();
ChangeTargetFrameworkAfterAppCreation(Path.Combine(TestSettings.TestArtifactsDirectory, testName, "MinimalTestApp"));
var publishCommand =
new DotnetCommand(_testOutput, "publish", "-bl", "MinimalTestApp", "-r", rid, "-f", _oldFramework, "-c", "Debug")
.WithWorkingDirectory(workingDirectory);
publishCommand.Execute()
.Should().Pass();
string publishDirectory = Path.Join(workingDirectory, "MinimalTestApp", "bin", "Debug", _oldFramework, rid, "publish");
return publishDirectory;
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async Task EndToEnd_MultiProjectSolution()
{
ILogger logger = _loggerFactory.CreateLogger(nameof(EndToEnd_MultiProjectSolution));
DirectoryInfo newSolutionDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, nameof(EndToEnd_MultiProjectSolution)));
if (newSolutionDir.Exists)
{
newSolutionDir.Delete(recursive: true);
}
newSolutionDir.Create();
// Create solution with projects
new DotnetNewCommand(_testOutput, "sln", "-n", nameof(EndToEnd_MultiProjectSolution))
.WithVirtualHive()
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetNewCommand(_testOutput, "console", "-n", "ConsoleApp")
.WithVirtualHive()
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "sln", "add", Path.Combine("ConsoleApp", "ConsoleApp.csproj"))
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetNewCommand(_testOutput, "web", "-n", "WebApp")
.WithVirtualHive()
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "sln", "add", Path.Combine("WebApp", "WebApp.csproj"))
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
// set TFM for the console app
using (FileStream stream = File.Open(Path.Join(newSolutionDir.FullName, "ConsoleApp", "ConsoleApp.csproj"), FileMode.Open, FileAccess.ReadWrite))
{
XDocument document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
document
.Descendants()
.First(e => e.Name.LocalName == "TargetFramework")
.Value = _oldFramework;
stream.SetLength(0);
await document.SaveAsync(stream, SaveOptions.None, CancellationToken.None);
}
// Set TFM for WebApp
using (FileStream stream = File.Open(Path.Join(newSolutionDir.FullName, "WebApp", "WebApp.csproj"), FileMode.Open, FileAccess.ReadWrite))
{
XDocument document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
document
.Descendants()
.First(e => e.Name.LocalName == "TargetFramework")
.Value = _oldFramework;
stream.SetLength(0);
await document.SaveAsync(stream, SaveOptions.None, CancellationToken.None);
}
// Publish
CommandResult commandResult = new DotnetCommand(_testOutput, "publish", "/t:PublishContainer")
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute();
commandResult.Should().Pass();
commandResult.Should().HaveStdOutContaining("Pushed image 'webapp:latest'");
commandResult.Should().HaveStdOutContaining("Pushed image 'consoleapp:latest'");
}
/// <summary>
/// Tests that a multi-project solution with a library that targets multiple frameworks can be published.
/// This is interesting because before https://github.com/dotnet/sdk/pull/47693 the container targets
/// wouldn't be loaded for multi-TFM project evaluations, so any calls to the PublishContainer target
/// for libraries (which may be multi-targeted even when referenced from a single-target published app project) would fail.
/// It's safe to load the target for libraries in a multi-targeted context because libraries don't have EnableSdkContainerSupport
/// enabled by default, so the target will be skipped.
/// </summary>
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async Task EndToEnd_MultiProjectSolution_with_multitargeted_library()
{
ILogger logger = _loggerFactory.CreateLogger(nameof(EndToEnd_MultiProjectSolution_with_multitargeted_library));
DirectoryInfo newSolutionDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, nameof(EndToEnd_MultiProjectSolution_with_multitargeted_library)));
if (newSolutionDir.Exists)
{
newSolutionDir.Delete(recursive: true);
}
newSolutionDir.Create();
// Create solution with projects
new DotnetNewCommand(_testOutput, "sln", "-n", nameof(EndToEnd_MultiProjectSolution_with_multitargeted_library))
.WithVirtualHive()
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetNewCommand(_testOutput, "web", "-n", "WebApp")
.WithVirtualHive()
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "sln", "add", Path.Combine("WebApp", "WebApp.csproj"))
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetNewCommand(_testOutput, "classlib", "-n", "Library")
.WithVirtualHive()
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "sln", "add", Path.Combine("Library", "Library.csproj"))
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute()
.Should().Pass();
// Set TFMs for Library - use current toolset + NS2.0 for compatibility
// also set IsPublishable to false
using (FileStream stream = File.Open(Path.Join(newSolutionDir.FullName, "Library", "Library.csproj"), FileMode.Open, FileAccess.ReadWrite))
{
XDocument document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
var tfmNode =
document
.Descendants()
.First(e => e.Name.LocalName == "TargetFramework");
var propertyGroupNode = tfmNode.Parent!;
tfmNode.Remove();
propertyGroupNode.Add(new XElement("TargetFrameworks", $"{ToolsetInfo.CurrentTargetFramework};netstandard2.0"));
propertyGroupNode.Add(new XElement("IsPublishable", "false"));
stream.SetLength(0);
await document.SaveAsync(stream, SaveOptions.None, CancellationToken.None);
}
// Publish
CommandResult commandResult = new DotnetCommand(_testOutput, "publish", "/t:PublishContainer")
.WithWorkingDirectory(newSolutionDir.FullName)
.Execute();
commandResult.Should().Pass();
commandResult.Should().HaveStdOutContaining("Pushed image 'webapp:latest'");
}
[DockerAvailableTheory(Skip = "https://github.com/dotnet/sdk/issues/49502")]
[InlineData("webapi", false)]
[InlineData("webapi", true)]
[InlineData("worker", false)]
[InlineData("worker", true)]
[InlineData("console", true)]
[InlineData("console", false)]
public async Task EndToEnd_NoAPI_ProjectType(string projectType, bool addPackageReference)
{
DirectoryInfo newProjectDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, $"CreateNewImageTest_{projectType}_{addPackageReference}"));
DirectoryInfo privateNuGetAssets = new(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet"));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
if (privateNuGetAssets.Exists)
{
privateNuGetAssets.Delete(recursive: true);
}
newProjectDir.Create();
privateNuGetAssets.Create();
new DotnetNewCommand(_testOutput, projectType, "-f", ToolsetInfo.CurrentTargetFramework)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
// do not pollute the primary/global NuGet package store with the private package(s)
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.Execute()
.Should().Pass();
if (addPackageReference)
{
File.Copy(Path.Combine(TestContext.Current.TestExecutionDirectory, "NuGet.config"), Path.Combine(newProjectDir.FullName, "NuGet.config"));
(string? packagePath, string? packageVersion) = ToolsetUtils.GetContainersPackagePath();
new DotnetCommand(_testOutput, "nuget", "add", "source", Path.GetDirectoryName(packagePath) ?? string.Empty, "--name", "local-temp")
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
// Add package to the project
new DotnetCommand(_testOutput, "add", "package", "Microsoft.NET.Build.Containers", "-f", ToolsetInfo.CurrentTargetFramework, "-v", packageVersion ?? string.Empty)
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
}
else
{
string projectPath = Path.Combine(newProjectDir.FullName, newProjectDir.Name + ".csproj");
var project = XDocument.Load(projectPath);
var ns = project.Root?.Name.Namespace ?? throw new InvalidOperationException("Project file is empty");
project.Save(projectPath);
}
string imageName = NewImageName();
string imageTag = $"1.0-{projectType}-{addPackageReference}";
// Build & publish the project
CommandResult commandResult = new DotnetCommand(
_testOutput,
"publish",
"/t:PublishContainer",
"/p:RuntimeIdentifier=linux-x64",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={imageTag}",
"/p:UseRazorSourceGenerator=false")
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
commandResult.Should().Pass();
if (addPackageReference)
{
commandResult.Should().HaveStdOutContaining("warning CONTAINER005: The Microsoft.NET.Build.Containers NuGet package is explicitly referenced but the current SDK can natively publish the project as a container. Consider removing the package reference to Microsoft.NET.Build.Containers because it is no longer needed.");
}
else
{
commandResult.Should().NotHaveStdOutContaining("warning");
}
ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
.Execute()
.Should().Pass();
var containerName = $"test-container-1-{projectType}-{addPackageReference}";
CommandResult processResult = ContainerCli.RunCommand(
_testOutput,
[
"--rm",
"--name",
containerName,
"-P",
..projectType != "console" ? ["--detach"] : new string[]{},
$"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}"
])
.Execute();
processResult.Should().Pass();
Assert.NotNull(processResult.StdOut);
string appContainerId = processResult.StdOut.Trim();
bool everSucceeded = false;
if (projectType == "webapi")
{
var portCommand =
ContainerCli.PortCommand(_testOutput, containerName, 8080)
.Execute();
portCommand.Should().Pass();
var port = portCommand.StdOut?.Trim().Split("\n")[0]; // only take the first port, which should be 0.0.0.0:PORT. the second line will be an ip6 port, if any.
_testOutput.WriteLine($"Discovered port was '{port}'");
var tempUri = new Uri($"http://{port}", UriKind.Absolute);
var appUri = new UriBuilder(tempUri)
{
Host = "localhost"
}.Uri;
HttpClient client = new();
client.BaseAddress = appUri;
// Give the server a moment to catch up, but no more than necessary.
for (int retry = 0; retry < 10; retry++)
{
try
{
var response = await client.GetAsync($"weatherforecast").ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
everSucceeded = true;
break;
}
}
catch { }
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
ContainerCli.LogsCommand(_testOutput, appContainerId)
.Execute()
.Should().Pass();
Assert.True(everSucceeded, $"{appUri}weatherforecast never responded.");
ContainerCli.StopCommand(_testOutput, appContainerId)
.Execute()
.Should().Pass();
}
else if (projectType == "worker")
{
// the worker template needs a second to start up and emit the logs we are looking for
await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
ContainerCli.LogsCommand(_testOutput, appContainerId)
.Execute()
.Should().Pass()
.And.HaveStdOutContaining("Worker running at");
ContainerCli.StopCommand(_testOutput, appContainerId)
.Execute()
.Should().Pass();
}
else if (projectType == "console")
{
processResult.Should().Pass().And.HaveStdOutContaining("Hello, World!");
}
newProjectDir.Delete(true);
privateNuGetAssets.Delete(true);
}
[DockerAvailableTheory(Skip = "https://github.com/dotnet/sdk/issues/49502")]
[InlineData(DockerRegistryManager.FullyQualifiedBaseImageAspNet)]
[InlineData(DockerRegistryManager.FullyQualifiedBaseImageAspNetDigest)]
public void EndToEnd_NoAPI_Console(string baseImage)
{
DirectoryInfo newProjectDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, "CreateNewImageTest"));
DirectoryInfo privateNuGetAssets = new(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet"));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
if (privateNuGetAssets.Exists)
{
privateNuGetAssets.Delete(recursive: true);
}
newProjectDir.Create();
privateNuGetAssets.Create();
new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
// do not pollute the primary/global NuGet package store with the private package(s)
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.Execute()
.Should().Pass();
ChangeTargetFrameworkAfterAppCreation(newProjectDir.FullName);
File.Copy(Path.Combine(TestContext.Current.TestExecutionDirectory, "NuGet.config"), Path.Combine(newProjectDir.FullName, "NuGet.config"));
(string? packagePath, string? packageVersion) = ToolsetUtils.GetContainersPackagePath();
new DotnetCommand(_testOutput, "nuget", "add", "source", Path.GetDirectoryName(packagePath) ?? string.Empty, "--name", "local-temp")
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
// Add package to the project
new DotnetCommand(_testOutput, "add", "package", "Microsoft.NET.Build.Containers", "-f", _oldFramework, "-v", packageVersion ?? string.Empty)
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
string imageName = NewImageName();
string imageTag = "1.0";
// Build & publish the project
new DotnetCommand(
_testOutput,
"publish",
"/t:PublishContainer",
"/p:runtimeidentifier=linux-x64",
$"/p:ContainerBaseImage={baseImage}",
$"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={imageTag}")
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
.Execute()
.Should().Pass();
var containerName = "test-container-2";
CommandResult processResult = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--name",
containerName,
$"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
.Execute();
processResult.Should().Pass().And.HaveStdOut("Hello, World!");
newProjectDir.Delete(true);
privateNuGetAssets.Delete(true);
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public void EndToEnd_SingleArch_NoRid()
{
// Create a new console project
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
string imageName = NewImageName();
string imageTag = "1.0";
// Run PublishContainer for multi-arch
CommandResult commandResult = new DotnetCommand(
_testOutput,
"publish",
"/t:PublishContainer",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={imageTag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
commandResult.Should().Pass();
// Check that the containers can be run
CommandResult processResultX64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--name",
$"test-container-singlearch-norid",
$"{imageName}:{imageTag}")
.Execute();
processResultX64.Should().Pass().And.HaveStdOut("Hello, World!");
}
/**
[InlineData("endtoendmultiarch-localregisty")]
[InlineData("myteam/endtoendmultiarch-localregisty")]
[DockerIsAvailableAndSupportsArchTheory(Skip = "https://github.com/dotnet/sdk/issues/49502", "linux/arm64", checkContainerdStoreAvailability: true)]
public void EndToEndMultiArch_LocalRegistry(string imageName)
{
string tag = "1.0";
string image = $"{imageName}:{tag}";
// Create a new console project
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
// Run PublishContainer for multi-arch
CommandResult commandResult = new DotnetCommand(
_testOutput,
"build",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={tag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
// Check that the app was published for each RID, one image was created locally
commandResult.Should().Pass()
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-x64"))
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-arm64"))
.And.HaveStdOutContaining($"Building image '{imageName}' for runtime identifier 'linux-x64'")
.And.HaveStdOutContaining($"Building image '{imageName}' for runtime identifier 'linux-arm64'")
.And.HaveStdOutContaining($"Pushed image '{image}' to local registry");
// Check that the containers can be run
CommandResult processResultX64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/amd64",
"--name",
$"test-container-{imageName.Replace('/', '-')}-x64",
image)
.Execute();
processResultX64.Should().Pass().And.HaveStdOut("Hello, World!");
CommandResult processResultArm64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/arm64",
"--name",
$"test-container-{imageName.Replace('/', '-')}-arm64",
image)
.Execute();
processResultArm64.Should().Pass().And.HaveStdOut("Hello, World!");
// Cleanup
newProjectDir.Delete(true);
}
*/
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public void MultiArchStillAllowsSingleRID()
{
string imageName = NewImageName();
string imageTag = "1.0";
string qualifiedImageName = $"{imageName}:{imageTag}";
// Create a new console project
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
// Run PublishContainer for multi-arch-capable, but single-arch actual
CommandResult commandResult = new DotnetCommand(
_testOutput,
"publish",
"/t:PublishContainer",
// make it so the app is _able_ to target both linux TFMs
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
// and that it opts into to multi-targeting containers for both of those linux TFMs
"/p:ContainerRuntimeIdentifiers=\"linux-x64;linux-arm64\"",
// but then only actually publishes for one of them
"/p:ContainerRuntimeIdentifier=linux-x64",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={imageTag}",
"/bl")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
// Check that the app was published for each RID,
// images were created locally for each RID
// and image index was NOT created
commandResult.Should().Pass()
// no rid-specific path because we didn't pass RuntimeIdentifier
.And.NotHaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-x64"))
.And.HaveStdOutContaining($"Pushed image '{qualifiedImageName}' to local registry")
.And.NotHaveStdOutContaining("Pushed image index");
// Check that the containers can be run
CommandResult processResultX64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--name",
$"test-container-{imageName}",
qualifiedImageName)
.Execute();
processResultX64.Should().Pass().And.HaveStdOut("Hello, World!");
// Cleanup
newProjectDir.Delete(true);
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public void MultiArchStillAllowsSingleRIDUsingJustRIDProperties()
{
string imageName = NewImageName();
string imageTag = "1.0";
string qualifiedImageName = $"{imageName}:{imageTag}";
// Create a new console project
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
// Run PublishContainer for multi-arch-capable, but single-arch actual
CommandResult commandResult = new DotnetCommand(
_testOutput,
"publish",
"/t:PublishContainer",
// make it so the app is _able_ to target both linux TFMs
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
// but then only actually publishes for one of them
"-r", "linux-x64",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={imageTag}",
"/bl")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
// Check that the app was published for each RID,
// images were created locally for each RID
// and image index was NOT created
commandResult.Should().Pass()
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-x64", configuration: "Release"))
.And.NotHaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-arm64", configuration: "Release"))
.And.HaveStdOutContaining($"Pushed image '{qualifiedImageName}' to local registry")
.And.NotHaveStdOutContaining("Pushed image index");
// Check that the containers can be run
CommandResult processResultX64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--name",
$"test-container-{imageName}-x64",
qualifiedImageName)
.Execute();
processResultX64.Should().Pass().And.HaveStdOut("Hello, World!");
// Cleanup
newProjectDir.Delete(true);
}
private DirectoryInfo CreateNewProject(string template, string tfm = ToolsetInfo.CurrentTargetFramework, [CallerMemberName] string callerMemberName = "")
{
DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, callerMemberName));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetNewCommand(_testOutput, template, "-f", tfm)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
return newProjectDir;
}
private string GetPublishArtifactsPath(string projectDir, string tfm, string rid, string configuration = "Debug")
=> Path.Combine(projectDir, "bin", configuration, tfm, rid, "publish");
/**
[InlineData("endtoendmultiarch-archivepublishing")]
[InlineData("myteam/endtoendmultiarch-archivepublishing")]
[DockerIsAvailableAndSupportsArchTheory(Skip = "https://github.com/dotnet/sdk/issues/49502", "linux/arm64", checkContainerdStoreAvailability: true)]
public void EndToEndMultiArch_ArchivePublishing(string imageName)
{
string tag = "1.0";
string image = $"{imageName}:{tag}";
string archiveOutput = TestSettings.TestArtifactsDirectory;
string imageTarball = Path.Combine(archiveOutput, $"{imageName}.tar.gz");
// Create a new console project
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
// Run PublishContainer for multi-arch with ContainerArchiveOutputPath
CommandResult commandResult = new DotnetCommand(
_testOutput,
"build",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
$"/p:ContainerArchiveOutputPath={archiveOutput}",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={tag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
// Check that the app was published for each RID, one image was created in local archive
commandResult.Should().Pass()
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-x64"))
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-arm64"))
.And.HaveStdOutContaining($"Building image '{imageName}' for runtime identifier 'linux-x64'")
.And.HaveStdOutContaining($"Building image '{imageName}' for runtime identifier 'linux-arm64'")
.And.HaveStdOutContaining($"Pushed image '{image}' to local archive at '{imageTarball}'");
// Check that tarball were created
File.Exists(imageTarball).Should().BeTrue();
// Load the multi-arch image from the tarball
ContainerCli.LoadCommand(_testOutput, "--input", imageTarball)
.Execute()
.Should().Pass();
// Check that the containers can be run
CommandResult processResultX64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/amd64",
"--name",
$"test-container-{imageName.Replace('/', '-')}-x64",
image)
.Execute();
processResultX64.Should().Pass().And.HaveStdOut("Hello, World!");
CommandResult processResultArm64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/arm64",
"--name",
$"test-container-{imageName.Replace('/', '-')}-arm64",
image)
.Execute();
processResultArm64.Should().Pass().And.HaveStdOut("Hello, World!");
// Cleanup
newProjectDir.Delete(true);
}
*/
[DockerIsAvailableAndSupportsArchFact("linux/arm64", checkContainerdStoreAvailability: true)]
public void EndToEndMultiArch_RemoteRegistry()
{
string imageName = NewImageName();
string imageTag = "1.0";
string registry = DockerRegistryManager.LocalRegistry;
string imageX64 = $"{imageName}:{imageTag}-linux-x64";
string imageArm64 = $"{imageName}:{imageTag}-linux-arm64";
string imageIndex = $"{imageName}:{imageTag}";
string imageFromRegistry = $"{registry}/{imageIndex}";
// Create a new console project
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
// Run PublishContainer for multi-arch with ContainerRegistry
CommandResult commandResult = new DotnetCommand(
_testOutput,
"build",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRegistry={registry}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={imageTag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
// Check that the app was published for each RID,
// images for each RID were pushed to remote registry
// and image index was pushed to remote registry
commandResult.Should().Pass()
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-x64"))
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-arm64"))
.And.HaveStdOutContaining($"Pushed image '{imageX64}' to registry '{registry}'.")
.And.HaveStdOutContaining($"Pushed image '{imageArm64}' to registry '{registry}'.")
.And.HaveStdOutContaining($"Pushed image index '{imageIndex}' to registry '{registry}'.");
// Check that the containers can be run
// First pull the image from the registry for each platform
ContainerCli.PullCommand(
_testOutput,
"--platform",
"linux/amd64",
imageFromRegistry)
.Execute()
.Should().Pass();
ContainerCli.PullCommand(
_testOutput,
"--platform",
"linux/arm64",
imageFromRegistry)
.Execute()
.Should().Pass();
// Run the containers
ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/amd64",
"--name",
$"test-container-{imageName}-x64",
imageFromRegistry)
.Execute().Should().Pass().And.HaveStdOut("Hello, World!");
ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/arm64",
"--name",
$"test-container-{imageName}-arm64",
imageFromRegistry)
.Execute().Should().Pass().And.HaveStdOut("Hello, World!");
// Cleanup
newProjectDir.Delete(true);
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/45181")]
public void EndToEndMultiArch_ContainerRuntimeIdentifiersOverridesRuntimeIdentifiers()
{
// Create a new console project
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
string imageName = NewImageName();
string imageTag = "1.0";
// Run PublishContainer for multi-arch with ContainerRuntimeIdentifiers
// RuntimeIdentifiers should contain all the RIDs from ContainerRuntimeIdentifiers to be able to publish
CommandResult commandResult = new DotnetCommand(
_testOutput,
"build",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
"/p:ContainerRuntimeIdentifiers=linux-arm64",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={imageTag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
// Check that the app was published only for RID from ContainerRuntimeIdentifiers
// images were built only for RID for from ContainerRuntimeIdentifiers
commandResult.Should().Pass()
.And.NotHaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-x64"))
.And.HaveStdOutContaining(GetPublishArtifactsPath(newProjectDir.FullName, _oldFramework, "linux-arm64"))
.And.NotHaveStdOutContaining($"Building image '{imageName}' for runtime identifier 'linux-x64'")
.And.HaveStdOutContaining($"Building image '{imageName}' for runtime identifier 'linux-arm64'");
// Cleanup
newProjectDir.Delete(true);
}
[DockerIsAvailableAndSupportsArchFact("linux/arm64", checkContainerdStoreAvailability: true)]
public void EndToEndMultiArch_EnvVariables()
{
string imageName = NewImageName();
string tag = "1.0";
string image = $"{imageName}:{tag}";
// Create new console app, set ContainerEnvironmentVariables, and set to output env variable
DirectoryInfo newProjectDir = CreateNewProject("console", _oldFramework);
var csprojPath = Path.Combine(newProjectDir.FullName, $"{nameof(EndToEndMultiArch_EnvVariables)}.csproj");
var csprojContent = File.ReadAllText(csprojPath);
csprojContent = csprojContent.Replace("</Project>",
"""
<ItemGroup>
<ContainerEnvironmentVariable Include="GoodEnvVar" Value="Foo" />
<ContainerEnvironmentVariable Include="AnotherEnvVar" Value="Bar" />
</ItemGroup>
</Project>
""");
File.WriteAllText(csprojPath, csprojContent);
File.WriteAllText(Path.Combine(newProjectDir.FullName, "Program.cs"),
"""
Console.Write(Environment.GetEnvironmentVariable("GoodEnvVar"));
Console.Write(Environment.GetEnvironmentVariable("AnotherEnvVar"));
""");
// Run PublishContainer for multi-arch
new DotnetCommand(
_testOutput,
"build",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={tag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
// Check that the env var is printed for linux/amd64 platform
string containerNameX64 = $"test-container-{imageName}-x64";
CommandResult processResultX64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/amd64",
"--name",
containerNameX64,
image)
.Execute();
processResultX64.Should().Pass().And.HaveStdOut("FooBar");
// Check that the env var is printed for linux/arm64 platform
string containerNameArm64 = $"test-container-{imageName}-arm64";
CommandResult processResultArm64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/arm64",
"--name",
containerNameArm64,
image)
.Execute();
processResultArm64.Should().Pass().And.HaveStdOut("FooBar");
// Cleanup
newProjectDir.Delete(true);
}
[DockerIsAvailableAndSupportsArchFact("linux/arm64", checkContainerdStoreAvailability: true)]
public void EndToEndMultiArch_Ports()
{
string imageName = NewImageName();
string tag = "1.0";
string image = $"{imageName}:{tag}";
// Create new web app, set ContainerPort
DirectoryInfo newProjectDir = CreateNewProject("webapp", _oldFramework);
var csprojPath = Path.Combine(newProjectDir.FullName, $"{nameof(EndToEndMultiArch_Ports)}.csproj");
var csprojContent = File.ReadAllText(csprojPath);
csprojContent = csprojContent.Replace("</Project>",
"""
<ItemGroup>
<ContainerPort Include="8082" Type="tcp" />
<ContainerPort Include="8083" Type="tcp" />
</ItemGroup>
</Project>
""");
File.WriteAllText(csprojPath, csprojContent);
// Run PublishContainer for multi-arch
new DotnetCommand(
_testOutput,
"build",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={tag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
// Check that the ports are correct for linux/amd64 platform
var containerNameX64 = $"test-container-{imageName}-x64";
CommandResult processResultX64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/amd64",
"--name",
containerNameX64,
"-P",
"--detach",
image)
.Execute();
processResultX64.Should().Pass();
// 8080 is the default port
CheckPorts(containerNameX64, [8080, 8082, 8083], [8081]);
// Check that the ports are correct for linux/arm64 platform
var containerNameArm64 = $"test-container-{imageName}-arm64";
CommandResult processResultArm64 = ContainerCli.RunCommand(
_testOutput,
"--rm",
"--platform",
"linux/arm64",
"--name",
containerNameArm64,
"-P",
"--detach",
image)
.Execute();
processResultArm64.Should().Pass();
// 8080 is the default port
CheckPorts(containerNameArm64, [8080, 8082, 8083], [8081]);
// Cleanup
// we ran containers with detached option, so we need to stop them
ContainerCli.StopCommand(_testOutput, containerNameX64)
.Execute()
.Should().Pass();
ContainerCli.StopCommand(_testOutput, containerNameArm64)
.Execute()
.Should().Pass();
newProjectDir.Delete(true);
}
private void CheckPorts(string containerName, int[] correctPorts, int[] incorrectPorts)
{
foreach (var port in correctPorts)
{
// Check the provided port is available
ContainerCli.PortCommand(_testOutput, containerName, port)
.Execute().Should().Pass();
}
foreach (var port in incorrectPorts)
{
// Check that not provided port is not available
ContainerCli.PortCommand(_testOutput, containerName, port)
.Execute().Should().Fail();
}
}
[DockerAvailableFact(checkContainerdStoreAvailability: true)]
public void EndToEndMultiArch_Labels()
{
string imageName = NewImageName();
string tag = "1.0";
string image = $"{imageName}:{tag}";
// Create new console app
DirectoryInfo newProjectDir = CreateNewProject("webapp");
// Run PublishContainer for multi-arch with ContainerGenerateLabels
new DotnetCommand(
_testOutput,
"publish",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={tag}")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
// Check that labels are set
CommandResult inspectResult = ContainerCli.InspectCommand(
_testOutput,
"--format={{json .Config.Labels}}",
image)
.Execute();
inspectResult.Should().Pass();
var labels = JsonSerializer.Deserialize<Dictionary<string, string>>(inspectResult.StdOut ?? string.Empty);
labels.Should().NotBeNull().And.HaveCountGreaterThan(0);
labels!.Values.Should().AllSatisfy(value => value.Should().NotBeNullOrEmpty());
// Cleanup
newProjectDir.Delete(true);
}
[DockerSupportsArchInlineData("linux/arm/v7", "linux-arm", "/app")]
[DockerSupportsArchInlineData("linux/arm64/v8", "linux-arm64", "/app")]
[DockerSupportsArchInlineData("linux/386", "linux-x86", "/app", Skip = "There's no apphost for linux-x86 so we can't execute self-contained, and there's no .NET runtime base image for linux-x86 so we can't execute framework-dependent.")]
[DockerSupportsArchInlineData("windows/amd64", "win-x64", "C:\\app")]
[DockerSupportsArchInlineData("linux/amd64", "linux-x64", "/app")]
[DockerAvailableTheory(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, string rid, string workingDir)
{
ILogger logger = _loggerFactory.CreateLogger(nameof(CanPackageForAllSupportedContainerRIDs));
string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework, rid: rid);
// Build the image
Registry registry = new(DockerRegistryManager.BaseImageSource, logger, RegistryMode.Push);
var isWin = rid.StartsWith("win");
ImageBuilder? imageBuilder = await registry.GetImageManifestAsync(
DockerRegistryManager.RuntimeBaseImage,
isWin ? DockerRegistryManager.Net8PreviewWindowsSpecificImageTag : DockerRegistryManager.Net9ImageTag,
rid,
ToolsetUtils.RidGraphManifestPicker,
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);
Layer l = Layer.FromDirectory(publishDirectory, isWin ? "C:\\app" : "/app", isWin, imageBuilder.ManifestMediaType);
imageBuilder.AddLayer(l);
imageBuilder.SetWorkingDirectory(workingDir);
string[] entryPoint = DecideEntrypoint(rid, "MinimalTestApp", workingDir);
imageBuilder.SetEntrypointAndCmd(entryPoint, Array.Empty<string>());
BuiltImage builtImage = imageBuilder.Build();
// Load the image into the local registry
var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9ImageTag, null);
var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { rid });
await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);
// Run the image
ContainerCli.RunCommand(
_testOutput,
"--rm",
"--tty",
"--platform",
dockerPlatform,
$"{NewImageName()}:{rid}")
.Execute()
.Should()
.Pass();
static string[] DecideEntrypoint(string rid, string appName, string workingDir)
{
var binary = rid.StartsWith("win", StringComparison.Ordinal) ? $"{appName}.exe" : appName;
return new[] { $"{workingDir}/{binary}" };
}
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async void CheckDownloadErrorMessageWhenSourceRepositoryThrows()
{
var loggerFactory = new TestLoggerFactory(_testOutput);
var logger = loggerFactory.CreateLogger(nameof(CheckDownloadErrorMessageWhenSourceRepositoryThrows));
string rid = "win-x64";
string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework, rid: rid);
// Build the image
Registry registry = new(DockerRegistryManager.BaseImageSource, logger, RegistryMode.Push);
ImageBuilder? imageBuilder = await registry.GetImageManifestAsync(
DockerRegistryManager.RuntimeBaseImage,
DockerRegistryManager.Net8PreviewWindowsSpecificImageTag,
rid,
ToolsetUtils.RidGraphManifestPicker,
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);
Layer l = Layer.FromDirectory(publishDirectory, "C:\\app", true, imageBuilder.ManifestMediaType);
imageBuilder.AddLayer(l);
imageBuilder.SetWorkingDirectory("C:\\app");
string[] entryPoint = DecideEntrypoint(rid, "MinimalTestApp", "C:\\app");
imageBuilder.SetEntrypointAndCmd(entryPoint, Array.Empty<string>());
BuiltImage builtImage = imageBuilder.Build();
// Load the image into the local registry
var sourceReference = new SourceImageReference(registry, "some_random_image", DockerRegistryManager.Net9ImageTag, null);
string archivePath = Path.Combine(TestSettings.TestArtifactsDirectory, nameof(CheckDownloadErrorMessageWhenSourceRepositoryThrows));
var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archivePath), NewImageName(), new[] { rid });
(var taskLog, var errors) = SetupTaskLog();
var telemetry = new Telemetry(sourceReference, destinationReference, taskLog);
await ImagePublisher.PublishImageAsync(builtImage, sourceReference, destinationReference, taskLog, telemetry, CancellationToken.None)
.ConfigureAwait(false);
// Assert the error message
Assert.True(taskLog.HasLoggedErrors);
Assert.NotNull(errors);
Assert.Single(errors);
Assert.Contains("Unable to download image from the repository", errors[0]);
static string[] DecideEntrypoint(string rid, string appName, string workingDir)
{
var binary = rid.StartsWith("win", StringComparison.Ordinal) ? $"{appName}.exe" : appName;
return new[] { $"{workingDir}/{binary}" };
}
static (Microsoft.Build.Utilities.TaskLoggingHelper, List<string?> errors) SetupTaskLog()
{
// We can use any Task, we just need TaskLoggingHelper
Tasks.CreateNewImage cni = new();
List<string?> errors = new();
IBuildEngine buildEngine = A.Fake<IBuildEngine>();
A.CallTo(() => buildEngine.LogWarningEvent(A<BuildWarningEventArgs>.Ignored)).Invokes((BuildWarningEventArgs e) => errors.Add(e.Message));
A.CallTo(() => buildEngine.LogErrorEvent(A<BuildErrorEventArgs>.Ignored)).Invokes((BuildErrorEventArgs e) => errors.Add(e.Message));
A.CallTo(() => buildEngine.LogMessageEvent(A<BuildMessageEventArgs>.Ignored)).Invokes((BuildMessageEventArgs e) => errors.Add(e.Message));
cni.BuildEngine = buildEngine;
return (cni.Log, errors);
}
}
[DockerAvailableFact(checkContainerdStoreAvailability: true)]
public void EnforcesOciSchemaForMultiRIDTarballOutput()
{
string imageName = NewImageName();
string tag = "1.0";
// Create new console app
DirectoryInfo newProjectDir = CreateNewProject("webapp");
// Run PublishContainer for multi-arch with ContainerGenerateLabels
var publishResult = new DotnetCommand(
_testOutput,
"publish",
"/t:PublishContainer",
"/p:RuntimeIdentifiers=\"linux-x64;linux-arm64\"",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
$"/p:ContainerRepository={imageName}",
$"/p:ContainerImageTag={tag}",
"/p:EnableSdkContainerSupport=true",
"/p:ContainerArchiveOutputPath=archive.tar.gz",
"-getProperty:GeneratedImageIndex",
"-getItem:GeneratedContainers",
"/bl")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute();
publishResult.Should().Pass();
publishResult.StdOut.Should().NotBeNull();
var jsonDump = JsonDocument.Parse(publishResult.StdOut);
var index = JsonDocument.Parse(jsonDump.RootElement.GetProperty("Properties").GetProperty("GeneratedImageIndex").ToString());
var containers = jsonDump.RootElement.GetProperty("Items").GetProperty("GeneratedContainers").EnumerateArray().ToArray();
index.RootElement.GetProperty("mediaType").GetString().Should().Be("application/vnd.oci.image.index.v1+json");
containers.Should().HaveCount(2);
foreach (var container in containers)
{
container.GetProperty("ManifestMediaType").GetString().Should().Be("application/vnd.oci.image.manifest.v1+json");
}
// Cleanup
newProjectDir.Delete(true);
}
}
|