|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
using FakeItEasy;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.NET.Build.Containers.IntegrationTests;
using Microsoft.NET.Build.Containers.UnitTests;
namespace Microsoft.NET.Build.Containers.Tasks.IntegrationTests;
[Collection("Docker tests")]
public class CreateNewImageTests
{
private ITestOutputHelper _testOutput;
public CreateNewImageTests(ITestOutputHelper testOutput)
{
_testOutput = testOutput;
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public void CreateNewImage_Baseline()
{
DirectoryInfo newProjectDir = new(GetTestDirectoryName());
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "publish", "-c", "Release", "-r", "linux-arm64", "--no-self-contained")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
CreateNewImage task = new();
(IBuildEngine buildEngine, List<string?> errors) = SetupBuildEngine();
task.BuildEngine = buildEngine;
task.BaseRegistry = "mcr.microsoft.com";
task.BaseImageName = "dotnet/runtime";
task.BaseImageTag = "7.0";
task.OutputRegistry = "localhost:5010";
task.LocalRegistry = DockerAvailableFactAttribute.LocalRegistry;
task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "Release", ToolsetInfo.CurrentTargetFramework, "linux-arm64", "publish");
task.Repository = "dotnet/create-new-image-baseline";
task.ImageTags = new[] { "latest" };
task.WorkingDirectory = "app/";
task.ContainerRuntimeIdentifier = "linux-arm64";
task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") };
task.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
Assert.True(task.Execute(), FormatBuildMessages(errors));
newProjectDir.Delete(true);
}
private static ImageConfig GetImageConfigFromTask(CreateNewImage task)
{
return new(task.GeneratedContainerConfiguration);
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public void ParseContainerProperties_EndToEnd()
{
DirectoryInfo newProjectDir = new(GetTestDirectoryName());
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "build", "--configuration", "release")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
ParseContainerProperties pcp = new();
(IBuildEngine buildEngine, List<string?> errors) = SetupBuildEngine();
pcp.BuildEngine = buildEngine;
pcp.FullyQualifiedBaseImageName = "mcr.microsoft.com/dotnet/runtime:7.0";
pcp.ContainerRegistry = "localhost:5010";
pcp.ContainerRepository = "dotnet/testimage";
pcp.ContainerImageTags = new[] { "5.0", "latest" };
Assert.True(pcp.Execute(), FormatBuildMessages(errors));
Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry);
Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage);
Assert.Equal("7.0", pcp.ParsedContainerTag);
Assert.Equal("dotnet/testimage", pcp.NewContainerRepository);
pcp.NewContainerTags.Should().BeEquivalentTo(new[] { "5.0", "latest" });
CreateNewImage cni = new();
(buildEngine, errors) = SetupBuildEngine();
cni.BuildEngine = buildEngine;
cni.BaseRegistry = pcp.ParsedContainerRegistry;
cni.BaseImageName = pcp.ParsedContainerImage;
cni.BaseImageTag = pcp.ParsedContainerTag;
cni.Repository = pcp.NewContainerRepository;
cni.OutputRegistry = "localhost:5010";
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", ToolsetInfo.CurrentTargetFramework);
cni.WorkingDirectory = "app/";
cni.Entrypoint = new TaskItem[] { new(newProjectDir.Name) };
cni.ImageTags = pcp.NewContainerTags;
cni.ContainerRuntimeIdentifier = "linux-x64";
cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
Assert.True(cni.Execute(), FormatBuildMessages(errors));
newProjectDir.Delete(true);
}
/// <summary>
/// Creates a console app that outputs the environment variable added to the image.
/// </summary>
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public void Tasks_EndToEnd_With_EnvironmentVariable_Validation()
{
DirectoryInfo newProjectDir = new(GetTestDirectoryName());
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
EndToEndTests.ChangeTargetFrameworkAfterAppCreation(newProjectDir.FullName);
File.WriteAllText(Path.Combine(newProjectDir.FullName, "Program.cs"), $"Console.Write(Environment.GetEnvironmentVariable(\"GoodEnvVar\"));");
new DotnetCommand(_testOutput, "build", "--configuration", "release", "/p:runtimeidentifier=linux-x64")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
ParseContainerProperties pcp = new();
(IBuildEngine buildEngine, List<string?> errors) = SetupBuildEngine();
pcp.BuildEngine = buildEngine;
pcp.FullyQualifiedBaseImageName = $"mcr.microsoft.com/{DockerRegistryManager.RuntimeBaseImage}:{DockerRegistryManager.Net9ImageTag}";
pcp.ContainerRegistry = "";
pcp.ContainerRepository = "dotnet/envvarvalidation";
pcp.ContainerImageTag = "latest";
Dictionary<string, string> dict = new();
dict.Add("Value", "Foo");
pcp.ContainerEnvironmentVariables = new[] { new TaskItem("B@dEnv.Var", dict), new TaskItem("GoodEnvVar", dict) };
Assert.True(pcp.Execute(), FormatBuildMessages(errors));
Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry);
Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage);
Assert.Equal(DockerRegistryManager.Net9ImageTag, pcp.ParsedContainerTag);
Assert.Single(pcp.NewContainerEnvironmentVariables);
Assert.Equal("Foo", pcp.NewContainerEnvironmentVariables[0].GetMetadata("Value"));
Assert.Equal("dotnet/envvarvalidation", pcp.NewContainerRepository);
Assert.Equal("latest", pcp.NewContainerTags[0]);
CreateNewImage cni = new();
(buildEngine, errors) = SetupBuildEngine();
cni.BuildEngine = buildEngine;
cni.BaseRegistry = pcp.ParsedContainerRegistry;
cni.BaseImageName = pcp.ParsedContainerImage;
cni.BaseImageTag = pcp.ParsedContainerTag;
cni.Repository = pcp.NewContainerRepository;
cni.OutputRegistry = pcp.NewContainerRegistry;
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", EndToEndTests._oldFramework, "linux-x64");
cni.WorkingDirectory = "/app";
cni.Entrypoint = new TaskItem[] { new($"/app/{newProjectDir.Name}") };
cni.ImageTags = pcp.NewContainerTags;
cni.ContainerEnvironmentVariables = pcp.NewContainerEnvironmentVariables;
cni.ContainerRuntimeIdentifier = "linux-x64";
cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
cni.LocalRegistry = DockerAvailableFactAttribute.LocalRegistry;
Assert.True(cni.Execute(), FormatBuildMessages(errors));
var config = GetImageConfigFromTask(cni);
// because we're building off of .net 8 images for this test, we can validate the user id and aspnet https urls
Assert.Equal("1654", config.GetUser());
var ports = config.Ports;
Assert.Single(ports);
Assert.Equal(new(8080, PortType.tcp), ports.First());
ContainerCli.RunCommand(_testOutput, "--rm", $"{pcp.NewContainerRepository}:latest")
.Execute()
.Should().Pass()
.And.HaveStdOut("Foo");
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public async System.Threading.Tasks.Task CreateNewImage_RootlessBaseImage()
{
const string RootlessBase = "dotnet/rootlessbase";
const string AppImage = "dotnet/testimagerootless";
const string RootlessUser = "1654";
var loggerFactory = new TestLoggerFactory(_testOutput);
var logger = loggerFactory.CreateLogger(nameof(CreateNewImage_RootlessBaseImage));
// Build a rootless base runtime image.
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();
var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net8ImageTag, null);
var destinationReference = new DestinationImageReference(registry, RootlessBase, new[] { "latest" });
await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false);
// Build an application image on top of the rootless base runtime image.
DirectoryInfo newProjectDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, nameof(CreateNewImage_RootlessBaseImage)));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "publish", "-c", "Release", "-r", "linux-x64", "--no-self-contained")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
CreateNewImage task = new();
var (buildEngine, errors) = SetupBuildEngine();
task.BuildEngine = buildEngine;
task.BaseRegistry = "localhost:5010";
task.BaseImageName = RootlessBase;
task.BaseImageTag = "latest";
task.OutputRegistry = "localhost:5010";
task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "Release", ToolsetInfo.CurrentTargetFramework, "linux-x64", "publish");
task.Repository = AppImage;
task.ImageTags = new[] { "latest" };
task.WorkingDirectory = "app/";
task.ContainerRuntimeIdentifier = "linux-x64";
task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") };
task.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
Assert.True(task.Execute());
newProjectDir.Delete(true);
// Verify the application image uses the non-root user from the base image.
imageBuilder = await registry.GetImageManifestAsync(
AppImage,
"latest",
"linux-x64",
ToolsetUtils.RidGraphManifestPicker,
cancellationToken: default).ConfigureAwait(false);
Assert.Equal(RootlessUser, imageBuilder.BaseImageConfig.GetUser());
}
[DockerAvailableFact(Skip = "https://github.com/dotnet/sdk/issues/49502")]
public void CanOverrideContainerImageFormat()
{
DirectoryInfo newProjectDir = new(GetTestDirectoryName());
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework)
.WithVirtualHive()
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "build", "--configuration", "release")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
ParseContainerProperties pcp = new();
(IBuildEngine buildEngine, List<string?> errors) = SetupBuildEngine();
pcp.BuildEngine = buildEngine;
pcp.FullyQualifiedBaseImageName = "mcr.microsoft.com/dotnet/runtime:9.0";
pcp.ContainerRegistry = "localhost:5010";
pcp.ContainerRepository = "dotnet/testimage";
pcp.ContainerImageTags = new[] { "5.0", "latest" };
Assert.True(pcp.Execute(), FormatBuildMessages(errors));
Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry);
Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage);
Assert.Equal("9.0", pcp.ParsedContainerTag);
Assert.Equal("dotnet/testimage", pcp.NewContainerRepository);
pcp.NewContainerTags.Should().BeEquivalentTo(new[] { "5.0", "latest" });
CreateNewImage cni = new();
(buildEngine, errors) = SetupBuildEngine();
cni.BuildEngine = buildEngine;
cni.BaseRegistry = pcp.ParsedContainerRegistry;
cni.BaseImageName = pcp.ParsedContainerImage;
cni.BaseImageTag = pcp.ParsedContainerTag;
cni.Repository = pcp.NewContainerRepository;
cni.OutputRegistry = "localhost:5010";
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", ToolsetInfo.CurrentTargetFramework);
cni.WorkingDirectory = "app/";
cni.Entrypoint = new TaskItem[] { new(newProjectDir.Name) };
cni.ImageTags = pcp.NewContainerTags;
cni.ContainerRuntimeIdentifier = "linux-x64";
cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
cni.ImageFormat = KnownImageFormats.OCI.ToString();
Assert.True(cni.Execute(), FormatBuildMessages(errors));
cni.GeneratedContainerMediaType.Should().Be(SchemaTypes.OciManifestV1);
newProjectDir.Delete(true);
}
private static (IBuildEngine buildEngine, List<string?> errors) SetupBuildEngine()
{
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));
return (buildEngine, errors);
}
private static string GetTestDirectoryName([CallerMemberName] string testName = "DefaultTest") => Path.Combine(TestSettings.TestArtifactsDirectory, testName + "_" + DateTime.Now.ToString("yyyyMMddHHmmss"));
private static string FormatBuildMessages(List<string?> messages) => string.Join("\r\n", messages);
}
|