EndToEndTests.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System.Runtime.CompilerServices;
  4. using Microsoft.DotNet.Cli.Utils;
  5. using Microsoft.NET.Build.Containers.LocalDaemons;
  6. using Microsoft.NET.Build.Containers.Resources;
  7. using Microsoft.NET.Build.Containers.UnitTests;
  8. using ILogger = Microsoft.Extensions.Logging.ILogger;
  9. namespace Microsoft.NET.Build.Containers.IntegrationTests;
  10. [Collection("Docker tests")]
  11. public class EndToEndTests : IDisposable
  12. {
  13. private ITestOutputHelper _testOutput;
  14. private readonly TestLoggerFactory _loggerFactory;
  15. public EndToEndTests(ITestOutputHelper testOutput)
  16. {
  17. _testOutput = testOutput;
  18. _loggerFactory = new TestLoggerFactory(testOutput);
  19. }
  20. public static string NewImageName([CallerMemberName] string callerMemberName = "")
  21. {
  22. var (normalizedName, warning, error) = ContainerHelpers.NormalizeRepository(callerMemberName);
  23. if (error is (var format, var args))
  24. {
  25. throw new ArgumentException(string.Format(Strings.ResourceManager.GetString(format)!, args));
  26. }
  27. return normalizedName!; // non-null if error is null
  28. }
  29. public void Dispose()
  30. {
  31. _loggerFactory.Dispose();
  32. }
  33. [DockerAvailableFact()]
  34. public async Task ApiEndToEndWithRegistryPushAndPull()
  35. {
  36. ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithRegistryPushAndPull));
  37. string publishDirectory = BuildLocalApp();
  38. // Build the image
  39. Registry registry = new(DockerRegistryManager.LocalRegistry, logger);
  40. ImageBuilder imageBuilder = await registry.GetImageManifestAsync(
  41. DockerRegistryManager.RuntimeBaseImage,
  42. DockerRegistryManager.Net9PreviewImageTag,
  43. "linux-x64",
  44. ToolsetUtils.RidGraphManifestPicker,
  45. cancellationToken: default).ConfigureAwait(false);
  46. Assert.NotNull(imageBuilder);
  47. Layer l = Layer.FromDirectory(publishDirectory, "/app", false, imageBuilder.ManifestMediaType);
  48. imageBuilder.AddLayer(l);
  49. imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty<string>());
  50. BuiltImage builtImage = imageBuilder.Build();
  51. // Push the image back to the local registry
  52. var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag);
  53. var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" });
  54. await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false);
  55. foreach (string tag in destinationReference.Tags)
  56. {
  57. // pull it back locally
  58. ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:{tag}")
  59. .Execute()
  60. .Should().Pass();
  61. // Run the image
  62. ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:{tag}")
  63. .Execute()
  64. .Should().Pass();
  65. }
  66. }
  67. [DockerAvailableFact()]
  68. public async Task ApiEndToEndWithLocalLoad()
  69. {
  70. ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithLocalLoad));
  71. string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework);
  72. // Build the image
  73. Registry registry = new(DockerRegistryManager.LocalRegistry, logger);
  74. ImageBuilder imageBuilder = await registry.GetImageManifestAsync(
  75. DockerRegistryManager.RuntimeBaseImage,
  76. DockerRegistryManager.Net9PreviewImageTag,
  77. "linux-x64",
  78. ToolsetUtils.RidGraphManifestPicker,
  79. cancellationToken: default).ConfigureAwait(false);
  80. Assert.NotNull(imageBuilder);
  81. Layer l = Layer.FromDirectory(publishDirectory, "/app", false, imageBuilder.ManifestMediaType);
  82. imageBuilder.AddLayer(l);
  83. imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty<string>());
  84. BuiltImage builtImage = imageBuilder.Build();
  85. // Load the image into the local registry
  86. var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag);
  87. var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { "latest", "1.0" });
  88. await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);
  89. // Run the image
  90. foreach (string tag in destinationReference.Tags)
  91. {
  92. ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{NewImageName()}:{tag}")
  93. .Execute()
  94. .Should().Pass();
  95. }
  96. }
  97. [DockerAvailableFact()]
  98. public async Task ApiEndToEndWithArchiveWritingAndLoad()
  99. {
  100. ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithArchiveWritingAndLoad));
  101. string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework);
  102. // Build the image
  103. Registry registry = new(DockerRegistryManager.LocalRegistry, logger);
  104. ImageBuilder imageBuilder = await registry.GetImageManifestAsync(
  105. DockerRegistryManager.RuntimeBaseImage,
  106. DockerRegistryManager.Net9PreviewImageTag,
  107. "linux-x64",
  108. ToolsetUtils.RidGraphManifestPicker,
  109. cancellationToken: default).ConfigureAwait(false);
  110. Assert.NotNull(imageBuilder);
  111. Layer l = Layer.FromDirectory(publishDirectory, "/app", false, imageBuilder.ManifestMediaType);
  112. imageBuilder.AddLayer(l);
  113. imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty<string>());
  114. BuiltImage builtImage = imageBuilder.Build();
  115. // Write the image to disk
  116. var archiveFile = Path.Combine(TestSettings.TestArtifactsDirectory,
  117. nameof(ApiEndToEndWithArchiveWritingAndLoad), "app.tar.gz");
  118. var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag);
  119. var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archiveFile), NewImageName(), new[] { "latest", "1.0" });
  120. await destinationReference.LocalRegistry!.LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);
  121. Assert.True(File.Exists(archiveFile), $"File.Exists({archiveFile})");
  122. // Load the archive
  123. ContainerCli.LoadCommand(_testOutput, "--input", archiveFile)
  124. .Execute()
  125. .Should().Pass();
  126. // Run the image
  127. foreach (string tag in destinationReference.Tags)
  128. {
  129. ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{NewImageName()}:{tag}")
  130. .Execute()
  131. .Should().Pass();
  132. }
  133. }
  134. private string BuildLocalApp([CallerMemberName] string testName = "TestName", string tfm = ToolsetInfo.CurrentTargetFramework, string rid = "linux-x64")
  135. {
  136. string workingDirectory = Path.Combine(TestSettings.TestArtifactsDirectory, testName);
  137. DirectoryInfo d = new(Path.Combine(workingDirectory, "MinimalTestApp"));
  138. if (d.Exists)
  139. {
  140. d.Delete(recursive: true);
  141. }
  142. Directory.CreateDirectory(workingDirectory);
  143. new DotnetNewCommand(_testOutput, "console", "-f", tfm, "-o", "MinimalTestApp")
  144. .WithVirtualHive()
  145. .WithWorkingDirectory(workingDirectory)
  146. .Execute()
  147. .Should().Pass();
  148. var publishCommand =
  149. new DotnetCommand(_testOutput, "publish", "-bl", "MinimalTestApp", "-r", rid, "-f", tfm, "-c", "Debug")
  150. .WithWorkingDirectory(workingDirectory);
  151. if (tfm == ToolsetInfo.CurrentTargetFramework)
  152. {
  153. publishCommand.Arguments.AddRange(new[] { "-p", $"RuntimeFrameworkVersion={DockerRegistryManager.RuntimeFrameworkVersion}" });
  154. }
  155. publishCommand.Execute()
  156. .Should().Pass();
  157. string publishDirectory = Path.Join(workingDirectory, "MinimalTestApp", "bin", "Debug", tfm, rid, "publish");
  158. return publishDirectory;
  159. }
  160. [DockerAvailableFact()]
  161. public async Task EndToEnd_MultiProjectSolution()
  162. {
  163. ILogger logger = _loggerFactory.CreateLogger(nameof(EndToEnd_MultiProjectSolution));
  164. DirectoryInfo newSolutionDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, $"CreateNewImageTest_EndToEnd_MultiProjectSolution"));
  165. if (newSolutionDir.Exists)
  166. {
  167. newSolutionDir.Delete(recursive: true);
  168. }
  169. newSolutionDir.Create();
  170. // Create solution with projects
  171. new DotnetNewCommand(_testOutput, "sln", "-n", nameof(EndToEnd_MultiProjectSolution))
  172. .WithVirtualHive()
  173. .WithWorkingDirectory(newSolutionDir.FullName)
  174. .Execute()
  175. .Should().Pass();
  176. new DotnetNewCommand(_testOutput, "console", "-n", "ConsoleApp")
  177. .WithVirtualHive()
  178. .WithWorkingDirectory(newSolutionDir.FullName)
  179. .Execute()
  180. .Should().Pass();
  181. new DotnetCommand(_testOutput, "sln", "add", Path.Combine("ConsoleApp", "ConsoleApp.csproj"))
  182. .WithWorkingDirectory(newSolutionDir.FullName)
  183. .Execute()
  184. .Should().Pass();
  185. new DotnetNewCommand(_testOutput, "web", "-n", "WebApp")
  186. .WithVirtualHive()
  187. .WithWorkingDirectory(newSolutionDir.FullName)
  188. .Execute()
  189. .Should().Pass();
  190. new DotnetCommand(_testOutput, "sln", "add", Path.Combine("WebApp", "WebApp.csproj"))
  191. .WithWorkingDirectory(newSolutionDir.FullName)
  192. .Execute()
  193. .Should().Pass();
  194. // Add 'EnableSdkContainerSupport' property to the ConsoleApp and set TFM
  195. using (FileStream stream = File.Open(Path.Join(newSolutionDir.FullName, "ConsoleApp", "ConsoleApp.csproj"), FileMode.Open, FileAccess.ReadWrite))
  196. {
  197. XDocument document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
  198. document
  199. .Descendants()
  200. .First(e => e.Name.LocalName == "PropertyGroup")?
  201. .Add(new XElement("EnableSdkContainerSupport", "true"));
  202. document
  203. .Descendants()
  204. .First(e => e.Name.LocalName == "TargetFramework")
  205. .Value = ToolsetInfo.CurrentTargetFramework;
  206. stream.SetLength(0);
  207. await document.SaveAsync(stream, SaveOptions.None, CancellationToken.None);
  208. }
  209. // Set TFM for WebApp
  210. using (FileStream stream = File.Open(Path.Join(newSolutionDir.FullName, "WebApp", "WebApp.csproj"), FileMode.Open, FileAccess.ReadWrite))
  211. {
  212. XDocument document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
  213. document
  214. .Descendants()
  215. .First(e => e.Name.LocalName == "TargetFramework")
  216. .Value = ToolsetInfo.CurrentTargetFramework;
  217. stream.SetLength(0);
  218. await document.SaveAsync(stream, SaveOptions.None, CancellationToken.None);
  219. }
  220. // Publish
  221. CommandResult commandResult = new DotnetCommand(_testOutput, "publish", "/t:PublishContainer")
  222. .WithWorkingDirectory(newSolutionDir.FullName)
  223. .Execute();
  224. commandResult.Should().Pass();
  225. commandResult.Should().HaveStdOutContaining("Pushed image 'webapp:latest'");
  226. commandResult.Should().HaveStdOutContaining("Pushed image 'consoleapp:latest'");
  227. }
  228. [DockerAvailableTheory()]
  229. [InlineData("webapi", false)]
  230. [InlineData("webapi", true)]
  231. [InlineData("worker", false)]
  232. [InlineData("worker", true)]
  233. public async Task EndToEnd_NoAPI_ProjectType(string projectType, bool addPackageReference)
  234. {
  235. DirectoryInfo newProjectDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, $"CreateNewImageTest_{projectType}_{addPackageReference}"));
  236. DirectoryInfo privateNuGetAssets = new(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet"));
  237. if (newProjectDir.Exists)
  238. {
  239. newProjectDir.Delete(recursive: true);
  240. }
  241. if (privateNuGetAssets.Exists)
  242. {
  243. privateNuGetAssets.Delete(recursive: true);
  244. }
  245. newProjectDir.Create();
  246. privateNuGetAssets.Create();
  247. new DotnetNewCommand(_testOutput, projectType, "-f", ToolsetInfo.CurrentTargetFramework)
  248. .WithVirtualHive()
  249. .WithWorkingDirectory(newProjectDir.FullName)
  250. // do not pollute the primary/global NuGet package store with the private package(s)
  251. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  252. .Execute()
  253. .Should().Pass();
  254. if (addPackageReference)
  255. {
  256. File.Copy(Path.Combine(TestContext.Current.TestExecutionDirectory, "NuGet.config"), Path.Combine(newProjectDir.FullName, "NuGet.config"));
  257. (string packagePath, string packageVersion) = ToolsetUtils.GetContainersPackagePath();
  258. new DotnetCommand(_testOutput, "nuget", "add", "source", Path.GetDirectoryName(packagePath), "--name", "local-temp")
  259. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  260. .WithWorkingDirectory(newProjectDir.FullName)
  261. .Execute()
  262. .Should().Pass();
  263. // Add package to the project
  264. new DotnetCommand(_testOutput, "add", "package", "Microsoft.NET.Build.Containers", "-f", ToolsetInfo.CurrentTargetFramework, "-v", packageVersion)
  265. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  266. .WithWorkingDirectory(newProjectDir.FullName)
  267. .Execute()
  268. .Should().Pass();
  269. }
  270. else
  271. {
  272. string projectPath = Path.Combine(newProjectDir.FullName, newProjectDir.Name + ".csproj");
  273. var project = XDocument.Load(projectPath);
  274. var ns = project.Root?.Name.Namespace ?? throw new InvalidOperationException("Project file is empty");
  275. project.Root?.Add(new XElement("PropertyGroup", new XElement("EnableSDKContainerSupport", "true")));
  276. project.Save(projectPath);
  277. }
  278. string imageName = NewImageName();
  279. string imageTag = $"1.0-{projectType}-{addPackageReference}";
  280. // Build & publish the project
  281. CommandResult commandResult = new DotnetCommand(
  282. _testOutput,
  283. "publish",
  284. "/p:PublishProfile=DefaultContainer",
  285. "/p:RuntimeIdentifier=linux-x64",
  286. $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
  287. $"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}",
  288. $"/p:ContainerRepository={imageName}",
  289. $"/p:ContainerImageTag={imageTag}",
  290. "/p:UseRazorSourceGenerator=false",
  291. $"/p:RuntimeFrameworkVersion={DockerRegistryManager.RuntimeFrameworkVersion}")
  292. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  293. .WithWorkingDirectory(newProjectDir.FullName)
  294. .Execute();
  295. commandResult.Should().Pass();
  296. if (addPackageReference)
  297. {
  298. commandResult.Should().HaveStdOutContaining("warning : 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.");
  299. }
  300. else
  301. {
  302. commandResult.Should().NotHaveStdOutContaining("warning");
  303. }
  304. ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
  305. .Execute()
  306. .Should().Pass();
  307. var containerName = $"test-container-1-{projectType}-{addPackageReference}";
  308. CommandResult processResult = ContainerCli.RunCommand(
  309. _testOutput,
  310. "--rm",
  311. "--name",
  312. containerName,
  313. "-P",
  314. "--detach",
  315. $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
  316. .Execute();
  317. processResult.Should().Pass();
  318. Assert.NotNull(processResult.StdOut);
  319. string appContainerId = processResult.StdOut.Trim();
  320. bool everSucceeded = false;
  321. if (projectType == "webapi")
  322. {
  323. var portCommand =
  324. ContainerCli.PortCommand(_testOutput, containerName, 8080)
  325. .Execute();
  326. portCommand.Should().Pass();
  327. 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.
  328. _testOutput.WriteLine($"Discovered port was '{port}'");
  329. var tempUri = new Uri($"http://{port}", UriKind.Absolute);
  330. var appUri = new UriBuilder(tempUri)
  331. {
  332. Host = "localhost"
  333. }.Uri;
  334. HttpClient client = new();
  335. client.BaseAddress = appUri;
  336. // Give the server a moment to catch up, but no more than necessary.
  337. for (int retry = 0; retry < 10; retry++)
  338. {
  339. try
  340. {
  341. var response = await client.GetAsync($"weatherforecast").ConfigureAwait(false);
  342. if (response.IsSuccessStatusCode)
  343. {
  344. everSucceeded = true;
  345. break;
  346. }
  347. }
  348. catch { }
  349. await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
  350. }
  351. ContainerCli.LogsCommand(_testOutput, appContainerId)
  352. .Execute()
  353. .Should().Pass();
  354. Assert.True(everSucceeded, $"{appUri}weatherforecast never responded.");
  355. ContainerCli.StopCommand(_testOutput, appContainerId)
  356. .Execute()
  357. .Should().Pass();
  358. }
  359. else if (projectType == "worker")
  360. {
  361. // the worker template needs a second to start up and emit the logs we are looking for
  362. await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
  363. var containerLogs =
  364. ContainerCli.LogsCommand(_testOutput, appContainerId)
  365. .Execute()
  366. .Should().Pass()
  367. .And.HaveStdOutContaining("Worker running at");
  368. ContainerCli.StopCommand(_testOutput, appContainerId)
  369. .Execute()
  370. .Should().Pass();
  371. }
  372. else
  373. {
  374. throw new NotImplementedException("Unknown project type");
  375. }
  376. newProjectDir.Delete(true);
  377. privateNuGetAssets.Delete(true);
  378. }
  379. [DockerAvailableFact()]
  380. public void EndToEnd_NoAPI_Console()
  381. {
  382. DirectoryInfo newProjectDir = new(Path.Combine(TestSettings.TestArtifactsDirectory, "CreateNewImageTest"));
  383. DirectoryInfo privateNuGetAssets = new(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet"));
  384. if (newProjectDir.Exists)
  385. {
  386. newProjectDir.Delete(recursive: true);
  387. }
  388. if (privateNuGetAssets.Exists)
  389. {
  390. privateNuGetAssets.Delete(recursive: true);
  391. }
  392. newProjectDir.Create();
  393. privateNuGetAssets.Create();
  394. new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework)
  395. .WithVirtualHive()
  396. .WithWorkingDirectory(newProjectDir.FullName)
  397. // do not pollute the primary/global NuGet package store with the private package(s)
  398. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  399. .Execute()
  400. .Should().Pass();
  401. File.Copy(Path.Combine(TestContext.Current.TestExecutionDirectory, "NuGet.config"), Path.Combine(newProjectDir.FullName, "NuGet.config"));
  402. (string packagePath, string packageVersion) = ToolsetUtils.GetContainersPackagePath();
  403. new DotnetCommand(_testOutput, "nuget", "add", "source", Path.GetDirectoryName(packagePath), "--name", "local-temp")
  404. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  405. .WithWorkingDirectory(newProjectDir.FullName)
  406. .Execute()
  407. .Should().Pass();
  408. // Add package to the project
  409. new DotnetCommand(_testOutput, "add", "package", "Microsoft.NET.Build.Containers", "-f", ToolsetInfo.CurrentTargetFramework, "-v", packageVersion)
  410. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  411. .WithWorkingDirectory(newProjectDir.FullName)
  412. .Execute()
  413. .Should().Pass();
  414. string imageName = NewImageName();
  415. string imageTag = "1.0";
  416. // Build & publish the project
  417. new DotnetCommand(
  418. _testOutput,
  419. "publish",
  420. "/t:PublishContainer",
  421. "/p:runtimeidentifier=linux-x64",
  422. $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}",
  423. $"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}",
  424. $"/p:ContainerRepository={imageName}",
  425. $"/p:ContainerImageTag={imageTag}",
  426. "/p:EnableSdkContainerSupport=true",
  427. $"/p:RuntimeFrameworkVersion={DockerRegistryManager.RuntimeFrameworkVersion}")
  428. .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
  429. .WithWorkingDirectory(newProjectDir.FullName)
  430. .Execute()
  431. .Should().Pass();
  432. ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
  433. .Execute()
  434. .Should().Pass();
  435. var containerName = "test-container-2";
  436. CommandResult processResult = ContainerCli.RunCommand(
  437. _testOutput,
  438. "--rm",
  439. "--name",
  440. containerName,
  441. $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
  442. .Execute();
  443. processResult.Should().Pass().And.HaveStdOut("Hello, World!");
  444. newProjectDir.Delete(true);
  445. privateNuGetAssets.Delete(true);
  446. }
  447. [DockerSupportsArchInlineData("linux/arm/v7", "linux-arm", "/app")]
  448. [DockerSupportsArchInlineData("linux/arm64/v8", "linux-arm64", "/app")]
  449. [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.")]
  450. [DockerSupportsArchInlineData("windows/amd64", "win-x64", "C:\\app")]
  451. [DockerSupportsArchInlineData("linux/amd64", "linux-x64", "/app")]
  452. [DockerAvailableTheory()]
  453. public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, string rid, string workingDir)
  454. {
  455. ILogger logger = _loggerFactory.CreateLogger(nameof(CanPackageForAllSupportedContainerRIDs));
  456. string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework, rid: rid);
  457. // Build the image
  458. Registry registry = new(DockerRegistryManager.BaseImageSource, logger);
  459. var isWin = rid.StartsWith("win");
  460. ImageBuilder? imageBuilder = await registry.GetImageManifestAsync(
  461. DockerRegistryManager.RuntimeBaseImage,
  462. isWin ? DockerRegistryManager.Net8PreviewWindowsSpecificImageTag : DockerRegistryManager.Net9PreviewImageTag,
  463. rid,
  464. ToolsetUtils.RidGraphManifestPicker,
  465. cancellationToken: default).ConfigureAwait(false);
  466. Assert.NotNull(imageBuilder);
  467. Layer l = Layer.FromDirectory(publishDirectory, isWin ? "C:\\app" : "/app", isWin, imageBuilder.ManifestMediaType);
  468. imageBuilder.AddLayer(l);
  469. imageBuilder.SetWorkingDirectory(workingDir);
  470. string[] entryPoint = DecideEntrypoint(rid, "MinimalTestApp", workingDir);
  471. imageBuilder.SetEntrypointAndCmd(entryPoint, Array.Empty<string>());
  472. BuiltImage builtImage = imageBuilder.Build();
  473. // Load the image into the local registry
  474. var sourceReference = new SourceImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net9PreviewImageTag);
  475. var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { rid });
  476. await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);
  477. // Run the image
  478. ContainerCli.RunCommand(
  479. _testOutput,
  480. "--rm",
  481. "--tty",
  482. "--platform",
  483. dockerPlatform,
  484. $"{NewImageName()}:{rid}")
  485. .Execute()
  486. .Should()
  487. .Pass();
  488. static string[] DecideEntrypoint(string rid, string appName, string workingDir)
  489. {
  490. var binary = rid.StartsWith("win", StringComparison.Ordinal) ? $"{appName}.exe" : appName;
  491. return new[] { $"{workingDir}/{binary}" };
  492. }
  493. }
  494. }