From 433346e33557ebc7f00f1471b9a52857c1495e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zavark=C3=B3=20G=C3=A1bor?= Date: Sat, 4 Jul 2020 00:34:57 +0200 Subject: [PATCH 0001/1880] Build should not show the "Does not support publish of C++/CLI project targeting dotnet core" error message when IsPublishable is false When building & publishing a whole solution which contains a C++/CLI project, the publish shows the following error message: C:\Program Files\dotnet\sdk\3.1.301\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Publish.targets(1048,5): error NETSDK1117: Does not support publish of C++/CLI project targeting dotnet core. [C:\XXX\YYY.vcxproj] If you set the IsPublishable property to false, the error is still shown. In my opinion this should be checked only when the project will be published here: https://github.com/dotnet/sdk/blob/d9186282b30574e1de24939aa32b615e19c084a7/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L63-L65 --- .../targets/Microsoft.NET.Publish.targets | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets index ef381fb01e4d..ab30f595008b 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets @@ -1167,6 +1167,7 @@ Copyright (c) .NET Foundation. All rights reserved. ============================================================ --> Date: Sun, 20 Jun 2021 20:31:07 +0930 Subject: [PATCH 0002/1880] dotnet sln list: Display NoProjectsFound when there is only solutions folders --- src/Cli/dotnet/commands/dotnet-sln/list/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs index f08be2b54303..81c5edb98eca 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; using System.CommandLine.Parsing; using System.Linq; using Microsoft.DotNet.Cli; @@ -23,8 +22,9 @@ public ListProjectsInSolutionCommand( public override int Execute() { - SlnFile slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory); - if (slnFile.Projects.Count == 0) + var slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory); + var slnProjects = slnFile.Projects.Where(p => p.TypeGuid != ProjectTypeGuids.SolutionFolderGuid).ToArray(); + if (slnProjects.Length == 0) { Reporter.Output.WriteLine(CommonLocalizableStrings.NoProjectsFound); } @@ -32,7 +32,7 @@ public override int Execute() { Reporter.Output.WriteLine($"{LocalizableStrings.ProjectsHeader}"); Reporter.Output.WriteLine(new string('-', LocalizableStrings.ProjectsHeader.Length)); - foreach (var slnProject in slnFile.Projects.Where(p => p.TypeGuid != ProjectTypeGuids.SolutionFolderGuid)) + foreach (var slnProject in slnProjects) { Reporter.Output.WriteLine(slnProject.FilePath); } From e054a56d6e6e4b442b6cdaf3a62ced0f6a4d6794 Mon Sep 17 00:00:00 2001 From: Zero Date: Thu, 24 Jun 2021 14:50:11 +0930 Subject: [PATCH 0003/1880] dotnet sln list: Add the ability to list project solution folder paths --- src/Cli/dotnet/SlnProjectExtensions.cs | 38 +++++++++++++++++++ .../dotnet-sln/LocalizableStrings.resx | 8 +++- .../commands/dotnet-sln/list/Program.cs | 23 ++++++++--- .../commands/dotnet-sln/list/SlnListParser.cs | 6 ++- .../dotnet-sln/xlf/LocalizableStrings.cs.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.de.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.es.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.fr.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.it.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.ja.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.ko.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.pl.xlf | 10 +++++ .../xlf/LocalizableStrings.pt-BR.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.ru.xlf | 10 +++++ .../dotnet-sln/xlf/LocalizableStrings.tr.xlf | 10 +++++ .../xlf/LocalizableStrings.zh-Hans.xlf | 10 +++++ .../xlf/LocalizableStrings.zh-Hant.xlf | 10 +++++ .../dotnet-sln.Tests/GivenDotnetSlnList.cs | 21 ++++++++++ 18 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/Cli/dotnet/SlnProjectExtensions.cs diff --git a/src/Cli/dotnet/SlnProjectExtensions.cs b/src/Cli/dotnet/SlnProjectExtensions.cs new file mode 100644 index 000000000000..de7747f45ff3 --- /dev/null +++ b/src/Cli/dotnet/SlnProjectExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using System.Linq; +using Microsoft.DotNet.Cli.Sln.Internal; + +namespace Microsoft.DotNet.Tools.Common +{ + internal static class SlnProjectExtensions + { + public static string GetFullSolutionFolderPath(this SlnProject slnProject) + { + var slnFile = slnProject.ParentFile; + var nestedProjects = slnFile.Sections + .GetOrCreateSection("NestedProjects", SlnSectionType.PreProcess) + .Properties; + var solutionFolders = slnFile.Projects + .GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid) + .ToArray(); + + string path = slnProject.Name; + string id = slnProject.Id; + + // If the nested projects contains this project's id then it has a parent + // Traverse from the project to each parent prepending the solution folder to the path + while (nestedProjects.ContainsKey(id)) + { + id = nestedProjects[id]; + + string solutionFolderPath = solutionFolders.Single(p => p.Id == id).FilePath; + path = Path.Combine(solutionFolderPath, path); + } + + return path; + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-sln/LocalizableStrings.resx b/src/Cli/dotnet/commands/dotnet-sln/LocalizableStrings.resx index ab017264b83e..cfc102795a7b 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/LocalizableStrings.resx +++ b/src/Cli/dotnet/commands/dotnet-sln/LocalizableStrings.resx @@ -171,4 +171,10 @@ The --solution-folder and --in-root options cannot be used together; use only one of the options. - \ No newline at end of file + + Display solution folder paths. + + + Project(s) (Solution Folder) + + diff --git a/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs b/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs index 81c5edb98eca..508506238613 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/list/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.CommandLine.Parsing; using System.Linq; using Microsoft.DotNet.Cli; @@ -13,28 +14,38 @@ namespace Microsoft.DotNet.Tools.Sln.List internal class ListProjectsInSolutionCommand : CommandBase { private readonly string _fileOrDirectory; + private readonly bool _displaySolutionFolders; public ListProjectsInSolutionCommand( ParseResult parseResult) : base(parseResult) { _fileOrDirectory = parseResult.ValueForArgument(SlnCommandParser.SlnArgument); + _displaySolutionFolders = parseResult.ValueForOption(SlnListParser.SolutionFolderOption); } public override int Execute() { var slnFile = SlnFileFactory.CreateFromFileOrDirectory(_fileOrDirectory); - var slnProjects = slnFile.Projects.Where(p => p.TypeGuid != ProjectTypeGuids.SolutionFolderGuid).ToArray(); - if (slnProjects.Length == 0) + + string[] paths = slnFile.Projects + .GetProjectsNotOfType(ProjectTypeGuids.SolutionFolderGuid) + .Select(project => _displaySolutionFolders ? project.GetFullSolutionFolderPath() : project.FilePath) + .ToArray(); + + if (paths.Length == 0) { Reporter.Output.WriteLine(CommonLocalizableStrings.NoProjectsFound); } else { - Reporter.Output.WriteLine($"{LocalizableStrings.ProjectsHeader}"); - Reporter.Output.WriteLine(new string('-', LocalizableStrings.ProjectsHeader.Length)); - foreach (var slnProject in slnProjects) + Array.Sort(paths); + + string header = _displaySolutionFolders ? LocalizableStrings.ProjectsSolutionFolderHeader : LocalizableStrings.ProjectsHeader; + Reporter.Output.WriteLine($"{header}"); + Reporter.Output.WriteLine(new string('-', header.Length)); + foreach (string slnProject in paths) { - Reporter.Output.WriteLine(slnProject.FilePath); + Reporter.Output.WriteLine(slnProject); } } return 0; diff --git a/src/Cli/dotnet/commands/dotnet-sln/list/SlnListParser.cs b/src/Cli/dotnet/commands/dotnet-sln/list/SlnListParser.cs index bb63846531c9..d7f305c6d0b3 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/list/SlnListParser.cs +++ b/src/Cli/dotnet/commands/dotnet-sln/list/SlnListParser.cs @@ -2,16 +2,20 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine; -using LocalizableStrings = Microsoft.DotNet.Tools.Sln.LocalizableStrings; +using Microsoft.DotNet.Tools.Sln; namespace Microsoft.DotNet.Cli { public static class SlnListParser { + public static readonly Option SolutionFolderOption = new Option(new string[] { "-s", "--solution-folders" }, LocalizableStrings.ListSolutionFoldersArgumentDescription); + public static Command GetCommand() { var command = new Command("list", LocalizableStrings.ListAppFullName); + command.AddOption(SolutionFolderOption); + return command; } } diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.cs.xlf index 12e360d2b016..f7d9b2aadc7a 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.cs.xlf @@ -32,6 +32,16 @@ Umístěte projekt do kořene řešení, není potřeba vytvářet složku řešení. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Odebere ze souboru řešení jeden nebo více projektů. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.de.xlf index 83602192d48c..39e031ae9fc6 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.de.xlf @@ -32,6 +32,16 @@ Platzieren Sie das Projekt im Stamm der Projektmappe, statt einen Projektmappenordner zu erstellen. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Entfernt ein oder mehrere Projekte von einer Projektmappendatei. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.es.xlf index 71f94fac6a9c..ef9cafcfaee1 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.es.xlf @@ -32,6 +32,16 @@ Coloque el proyecto en la raíz de la solución, en lugar de crear una carpeta de soluciones. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Quita uno o varios proyectos de un archivo de solución. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.fr.xlf index d80b97c93310..c1fb59e15f0a 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.fr.xlf @@ -32,6 +32,16 @@ Place le projet à la racine de la solution, au lieu de créer un dossier solution. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Supprimez un ou plusieurs projets d'un fichier solution. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.it.xlf index 848ae5b23a71..ce70f5f6890a 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.it.xlf @@ -32,6 +32,16 @@ Inserisce il progetto nella radice della soluzione invece di creare una cartella soluzione. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Consente di rimuovere uno o più progetti da un file di soluzione. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ja.xlf index fd985e2fe900..ca6a7b0885e3 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ja.xlf @@ -32,6 +32,16 @@ ソリューション フォルダーを作成するのではなく、プロジェクトをソリューションのルートに配置します。 + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. 1 つ以上のプロジェクトをソリューション ファイルから削除します。 diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ko.xlf index 590fbb3aaa68..c44f510fa164 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ko.xlf @@ -32,6 +32,16 @@ 솔루션 폴더를 만드는 대신, 솔루션의 루트에 프로젝트를 배치하세요. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. 솔루션 파일에서 하나 이상의 프로젝트를 제거합니다. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pl.xlf index 2e92c2cb9161..a48b828bbbcb 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pl.xlf @@ -32,6 +32,16 @@ Umieść projekt w katalogu głównym rozwiązania zamiast tworzyć folder rozwiązania. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Usuń co najmniej jeden projekt z pliku rozwiązania. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pt-BR.xlf index 62b3cd6c31bd..8e182090492c 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.pt-BR.xlf @@ -32,6 +32,16 @@ Coloque o projeto na raiz da solução, em vez de criar uma pasta da solução. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Remover um ou mais projetos de um arquivo de solução. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ru.xlf index d367f0524d2c..0082257a1af0 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.ru.xlf @@ -32,6 +32,16 @@ Поместите проект в корень решения вместо создания папки решения. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Удаление проектов из файла решения. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.tr.xlf index 955c27a5dbd0..d07abbf87ca1 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.tr.xlf @@ -32,6 +32,16 @@ Bir çözüm klasörü oluşturmak yerine projeyi çözümün köküne yerleştirin. + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. Bir çözüm dosyasından bir veya daha fazla projeyi kaldırır. diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hans.xlf index ca08269d7de7..a55faab886fb 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hans.xlf @@ -32,6 +32,16 @@ 将项目放在解决方案的根目录下,而不是创建解决方案文件夹。 + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. 从解决方案文件中删除一个或多个项目。 diff --git a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hant.xlf index 34702935fc63..99b257ae5d81 100644 --- a/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/commands/dotnet-sln/xlf/LocalizableStrings.zh-Hant.xlf @@ -32,6 +32,16 @@ 請將專案放置在解決方案的根目錄中,而非放置於建立解決方案的資料夾中。 + + Display solution folder paths. + Display solution folder paths. + + + + Project(s) (Solution Folder) + Project(s) (Solution Folder) + + Remove one or more projects from a solution file. 從解決方案檔移除一或多個專案。 diff --git a/src/Tests/dotnet-sln.Tests/GivenDotnetSlnList.cs b/src/Tests/dotnet-sln.Tests/GivenDotnetSlnList.cs index 103ca2a14c71..ae12bdceb5f3 100644 --- a/src/Tests/dotnet-sln.Tests/GivenDotnetSlnList.cs +++ b/src/Tests/dotnet-sln.Tests/GivenDotnetSlnList.cs @@ -30,6 +30,7 @@ dotnet [options] sln list The solution file to operate on. If not specified, the command will search the current directory for one. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}] Options: + -s, --solution-folders Display solution folder paths. -?, -h, --help Show help and usage information"; public GivenDotnetSlnList(ITestOutputHelper log) : base(log) @@ -207,5 +208,25 @@ public void WhenProjectsPresentInTheReadonlySolutionItListsThem() cmd.Should().Pass(); cmd.StdOut.Should().BeVisuallyEquivalentTo(expectedOutput); } + + [Fact] + public void WhenProjectsInSolutionFoldersPresentInTheSolutionItListsSolutionFolderPaths() + { + var expectedOutput = $@"{CommandLocalizableStrings.ProjectsSolutionFolderHeader} +{new string('-', CommandLocalizableStrings.ProjectsSolutionFolderHeader.Length)} +ConsoleApp1 +{Path.Combine("NestedSolution", "NestedFolder", "NestedFolder", "ConsoleApp2")}"; + + var projectDirectory = _testAssetsManager + .CopyTestAsset("SlnFileWithSolutionItemsInNestedFolders") + .WithSource() + .Path; + + var cmd = new DotnetCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute("sln", "list", "--solution-folders"); + cmd.Should().Pass(); + cmd.StdOut.Should().BeVisuallyEquivalentTo(expectedOutput); + } } } From 7b605dc5a2e2cfaef94e2c5e3297d28ebffec49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Rodri=CC=81guez?= Date: Fri, 27 Aug 2021 03:20:45 -0700 Subject: [PATCH 0004/1880] Updated ListPackageReferencesCommand to read the file/directory the same way as ListProjectToProjectReferencesCommand. Added regression test. --- .../ListPackageReferencesCommand.cs | 4 +--- .../GivenDotnetListPackage.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-list/dotnet-list-package/ListPackageReferencesCommand.cs b/src/Cli/dotnet/commands/dotnet-list/dotnet-list-package/ListPackageReferencesCommand.cs index 02393e7e3f04..f4e8c722f8f5 100644 --- a/src/Cli/dotnet/commands/dotnet-list/dotnet-list-package/ListPackageReferencesCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-list/dotnet-list-package/ListPackageReferencesCommand.cs @@ -8,7 +8,6 @@ using System.Linq; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.Tools.Common; using Microsoft.DotNet.Tools.NuGet; namespace Microsoft.DotNet.Tools.List.PackageReferences @@ -21,8 +20,7 @@ internal class ListPackageReferencesCommand : CommandBase public ListPackageReferencesCommand( ParseResult parseResult) : base(parseResult) { - _fileOrDirectory = PathUtility.GetAbsolutePath(PathUtility.EnsureTrailingSlash(Directory.GetCurrentDirectory()), - parseResult.ValueForArgument(ListCommandParser.SlnOrProjectArgument)); + _fileOrDirectory = parseResult.ValueForArgument(ListCommandParser.SlnOrProjectArgument); } public override int Execute() diff --git a/src/Tests/dotnet-list-package.Tests/GivenDotnetListPackage.cs b/src/Tests/dotnet-list-package.Tests/GivenDotnetListPackage.cs index 9e7a87c0efd4..7df48959e905 100644 --- a/src/Tests/dotnet-list-package.Tests/GivenDotnetListPackage.cs +++ b/src/Tests/dotnet-list-package.Tests/GivenDotnetListPackage.cs @@ -303,5 +303,27 @@ public void ItEnforcesOptionRules(bool throws, params string[] options) checkRules(); // Test for no throw } } + + [UnixOnlyFact] + public void ItRunsInCurrentDirectoryWithPoundInPath() + { + // Regression test for https://github.com/dotnet/sdk/issues/19654 + var testAssetName = "TestAppSimple"; + var testAsset = _testAssetsManager + .CopyTestAsset(testAssetName, "C#") + .WithSource(); + var projectDirectory = testAsset.Path; + + new RestoreCommand(testAsset) + .Execute() + .Should() + .Pass(); + + new ListPackageCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute() + .Should() + .Pass(); + } } } From 7f191e7b180b9e4abeed8d430b9520ababcee07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Rodri=CC=81guez?= Date: Fri, 3 Sep 2021 03:10:57 -0700 Subject: [PATCH 0005/1880] Marked PathUtility.GetAbsolutePath as obsolete. --- src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs index a071754d7bba..835e55bcefb2 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs @@ -222,6 +222,7 @@ public static string GetRelativePath(string path1, string path2, char separator, return path; } + [Obsolete("Use System.IO.Path.GetFullPath(string, string) instead, or PathUtility.GetFullPath(string) if the base path is the current working directory.")] public static string GetAbsolutePath(string basePath, string relativePath) { if (basePath == null) From b9fbf8205abc12a3566d5f934a42cc577502492c Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 16 May 2022 09:25:09 -0500 Subject: [PATCH 0006/1880] Initial commit --- docs/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000000..42b13098dd99 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Documents Index + +This repo includes documents that explain how to use and build the product. It also provides detailed information about the project. + +Here is a good example of this file: https://github.com/dotnet/coreclr/blob/master/Documentation/README.md. + +This is where a TOC goes! \ No newline at end of file From b3acf7072fc646772333a70e7c3fb7e6cfb2d166 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Tue, 21 Jun 2022 15:59:55 -0500 Subject: [PATCH 0007/1880] A few doc links to get started --- docs/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 42b13098dd99..ee47bdc13b90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,8 @@ # Documents Index -This repo includes documents that explain how to use and build the product. It also provides detailed information about the project. +## References -Here is a good example of this file: https://github.com/dotnet/coreclr/blob/master/Documentation/README.md. - -This is where a TOC goes! \ No newline at end of file +* [OCI Image Format spec](https://github.com/opencontainers/image-spec/blob/main/spec.md) +* [Docker Registry API docs](https://docs.docker.com/registry/spec/api/) +* [Setting up a local Docker registry](https://docs.docker.com/registry/) +* [Tutorial: Containerize a .NET app](https://docs.microsoft.com/dotnet/core/docker/build-container?tabs=windows) From 5b23c8c9b75c80deff6095095e632674d2bff4ae Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Tue, 12 Jul 2022 14:17:51 -0500 Subject: [PATCH 0008/1880] Containerize cli --- containerize/Program.cs | 79 +++++++++++++++++++++ containerize/Properties/launchSettings.json | 8 +++ containerize/containerize.csproj | 18 +++++ 3 files changed, 105 insertions(+) create mode 100644 containerize/Program.cs create mode 100644 containerize/Properties/launchSettings.json create mode 100644 containerize/containerize.csproj diff --git a/containerize/Program.cs b/containerize/Program.cs new file mode 100644 index 000000000000..33594281ae7f --- /dev/null +++ b/containerize/Program.cs @@ -0,0 +1,79 @@ +using System.CommandLine; +using System.Containers; + +RootCommand rootCommand = new("Containerize an application without Docker."); + +Option fileOption = new( + name: "--folder", + description: "The folder to pack."); + +rootCommand.AddOption(fileOption); + +Option containerPath = new( + name: "--containerPath", + description: "Location of the packed folder in the final image."); + +rootCommand.AddOption(containerPath); + +Option registryUri = new( + name: "--registry", + description: "Location of the registry to push to."); + +rootCommand.AddOption(registryUri); + +Option baseImageName = new( + name: "--base", + description: "Base image name."); + +rootCommand.AddOption(baseImageName); + +Option baseImageTag = new( + name: "--baseTag", + description: "Base image tag."); + +rootCommand.AddOption(baseImageTag); + +Option entrypoint = new( + name: "--entrypoint", + description: "Entrypoint application."); + +rootCommand.AddOption(entrypoint); + +Option imageName = new( + name: "--name", + description: "Name of the new image."); + +rootCommand.AddOption(imageName); + +rootCommand.SetHandler(async (folder, cp, uri, baseImageName, baseTag, entrypoint, imageName) => +{ + await Containerize(folder.FullName, cp, uri, baseImageName, baseTag, entrypoint, imageName); +}, + fileOption, + containerPath, + registryUri, + baseImageName, + baseImageTag, + entrypoint, + imageName); + +return await rootCommand.InvokeAsync(args); + +async Task Containerize(string folder, string containerPath, string registryName, string baseName, string baseTag, string entrypoint, string imageName) +{ + Registry registry = new Registry(new Uri($"http://{registryName}")); + + Image x = await registry.GetImageManifest(baseName, baseTag); + + Layer l = Layer.FromDirectory(folder, "/app"); + + x.AddLayer(l); + + x.SetEntrypoint(entrypoint); + + // Push the image back to the local registry + + await registry.Push(x, imageName, baseName); + + Console.WriteLine($"Pushed {registryName}/{imageName}:latest"); +} \ No newline at end of file diff --git a/containerize/Properties/launchSettings.json b/containerize/Properties/launchSettings.json new file mode 100644 index 000000000000..d86a4bd48a83 --- /dev/null +++ b/containerize/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "containerize": { + "commandName": "Project", + "commandLineArgs": "--folder S:\\play\\helloworld6\\bin\\Debug\\net6.0\\publish --registry \"http://localhost:5000\" --base \"dotnet/sdk\" --baseTag \"6.0\" --entrypoint \"/app/helloworld6\" --name \"demo/container\"" + } + } +} \ No newline at end of file diff --git a/containerize/containerize.csproj b/containerize/containerize.csproj new file mode 100644 index 000000000000..c541d4e80d9e --- /dev/null +++ b/containerize/containerize.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + From 0a214300ea6f58301eaecd44c8be5ebeefb8ee28 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 14 Jul 2022 16:06:09 -0500 Subject: [PATCH 0009/1880] defaults for the CLI --- containerize/Program.cs | 63 +++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index 33594281ae7f..04f0094ad43f 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -1,71 +1,72 @@ using System.CommandLine; using System.Containers; -RootCommand rootCommand = new("Containerize an application without Docker."); - -Option fileOption = new( - name: "--folder", - description: "The folder to pack."); - -rootCommand.AddOption(fileOption); - -Option containerPath = new( - name: "--containerPath", - description: "Location of the packed folder in the final image."); - -rootCommand.AddOption(containerPath); +var fileOption = new Argument( + name: "folder", + description: "The folder to pack.") + .LegalFilePathsOnly().ExistingOnly(); Option registryUri = new( name: "--registry", - description: "Location of the registry to push to."); - -rootCommand.AddOption(registryUri); + description: "Location of the registry to push to.", + getDefaultValue: () => "localhost:5010"); Option baseImageName = new( name: "--base", - description: "Base image name."); - -rootCommand.AddOption(baseImageName); + description: "Base image name.", + getDefaultValue: () => "mcr.microsoft.com/dotnet/runtime"); Option baseImageTag = new( name: "--baseTag", - description: "Base image tag."); - -rootCommand.AddOption(baseImageTag); + description: "Base image tag.", + getDefaultValue: () => $"{System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription[5]}.0"); Option entrypoint = new( name: "--entrypoint", description: "Entrypoint application."); -rootCommand.AddOption(entrypoint); - Option imageName = new( name: "--name", description: "Name of the new image."); -rootCommand.AddOption(imageName); +var imageTag = new Option("--tag", description: "Tag of the new image.", getDefaultValue: () => "latest"); + +var workingDir = new Option("--working-dir", description: "The working directory of the application", getDefaultValue: () => "/app"); -rootCommand.SetHandler(async (folder, cp, uri, baseImageName, baseTag, entrypoint, imageName) => +RootCommand rootCommand = new("Containerize an application without Docker."){ + fileOption, + registryUri, + baseImageName, + baseImageTag, + entrypoint, + imageName, + imageTag +}; +rootCommand.SetHandler(async (folder, containerWorkingDir, uri, baseImageName, baseTag, entrypoint, imageName, imageTag) => { - await Containerize(folder.FullName, cp, uri, baseImageName, baseTag, entrypoint, imageName); + await Containerize(folder, containerWorkingDir, uri, baseImageName, baseTag, entrypoint, imageName, imageTag); }, fileOption, - containerPath, + workingDir, registryUri, baseImageName, baseImageTag, entrypoint, - imageName); + imageName, + imageTag + ); return await rootCommand.InvokeAsync(args); -async Task Containerize(string folder, string containerPath, string registryName, string baseName, string baseTag, string entrypoint, string imageName) +async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string entrypoint, string imageName, string imageTag) { Registry registry = new Registry(new Uri($"http://{registryName}")); + Console.WriteLine($"Reading from {registry.BaseUri}"); + Image x = await registry.GetImageManifest(baseName, baseTag); - Layer l = Layer.FromDirectory(folder, "/app"); + Layer l = Layer.FromDirectory(folder.FullName, workingDir); x.AddLayer(l); From 06702d8985d8f2380a9878853df7928ae2c60a40 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 14 Jul 2022 16:51:07 -0500 Subject: [PATCH 0010/1880] update to 5010 port for registry and preload image to docker --- containerize/Program.cs | 12 ++++++++++-- containerize/Properties/launchSettings.json | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index 04f0094ad43f..817254f231a1 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -14,7 +14,7 @@ Option baseImageName = new( name: "--base", description: "Base image name.", - getDefaultValue: () => "mcr.microsoft.com/dotnet/runtime"); + getDefaultValue: () => "dotnet/runtime"); Option baseImageTag = new( name: "--baseTag", @@ -40,7 +40,8 @@ baseImageTag, entrypoint, imageName, - imageTag + imageTag, + workingDir }; rootCommand.SetHandler(async (folder, containerWorkingDir, uri, baseImageName, baseTag, entrypoint, imageName, imageTag) => { @@ -66,6 +67,7 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry Image x = await registry.GetImageManifest(baseName, baseTag); + Console.WriteLine($"Copying from {folder.FullName} to {workingDir}"); Layer l = Layer.FromDirectory(folder.FullName, workingDir); x.AddLayer(l); @@ -77,4 +79,10 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry await registry.Push(x, imageName, baseName); Console.WriteLine($"Pushed {registryName}/{imageName}:latest"); + + var pullBase = System.Diagnostics.Process.Start("docker", $"pull {registryName}/{imageName}:latest"); + await pullBase.WaitForExitAsync(); + + Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run -rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); + } \ No newline at end of file diff --git a/containerize/Properties/launchSettings.json b/containerize/Properties/launchSettings.json index d86a4bd48a83..444d22dc6574 100644 --- a/containerize/Properties/launchSettings.json +++ b/containerize/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "containerize": { "commandName": "Project", - "commandLineArgs": "--folder S:\\play\\helloworld6\\bin\\Debug\\net6.0\\publish --registry \"http://localhost:5000\" --base \"dotnet/sdk\" --baseTag \"6.0\" --entrypoint \"/app/helloworld6\" --name \"demo/container\"" + "commandLineArgs": "--folder S:\\play\\helloworld6\\bin\\Debug\\net6.0\\publish --registry \"http://localhost:5010\" --base \"dotnet/sdk\" --baseTag \"6.0\" --entrypoint \"/app/helloworld6\" --name \"demo/container\"" } } } \ No newline at end of file From 928be05e7834864d2e80af9190835f5449820bc3 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 18 Jul 2022 15:49:31 -0500 Subject: [PATCH 0011/1880] Add docs for a demo thread anyone could run --- containerize/Program.cs | 2 +- docs/DEMO.md | 95 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 docs/DEMO.md diff --git a/containerize/Program.cs b/containerize/Program.cs index 817254f231a1..59a69163aced 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -83,6 +83,6 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry var pullBase = System.Diagnostics.Process.Start("docker", $"pull {registryName}/{imageName}:latest"); await pullBase.WaitForExitAsync(); - Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run -rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); + Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); } \ No newline at end of file diff --git a/docs/DEMO.md b/docs/DEMO.md new file mode 100644 index 000000000000..b3909aa1e92f --- /dev/null +++ b/docs/DEMO.md @@ -0,0 +1,95 @@ +# Containerize Demo + +this is a simple thread to go from zero to a containerized application using the `containerize` app from this repo. + +To perform this demo, you must have [Docker](https://www.docker.com/products/docker-desktop/) installed locally. + +## Set up your environment + +### Create the registry + +To containerize your app, we'll be using a local container registry until we get the end-to-end with the local Docker daemon set up. This means we'll need to run and seed that registry with the base images. To do so, run the following commands from the repo root: + +```shell +> dotnet build --no-restore -t:StartDockerRegistry +> docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +6900146c8604 registry:2 "/entrypoint.sh /etc…" 17 hours ago Up 15 seconds 0.0.0.0:5010->5000/tcp registry +``` + +You should see the 'registry' container running. This is where your images will be stored. + +### Preload base images + +Now seed the registry with the required base images: + +```shell +> dotnet build --no-restore -t:PreloadBaseImages +``` + +This command will show a lot of output, and it might take a minute. + +### Build the containerize app + +```shell +> cd containerize +> dotnet publish containerize +> $env:PATH="$env:PATH;$pwd\\bin\\Debug\\net7.0\\publish\\" +> containerize --help +``` + +## Create an app + +Create a new app in a location of your choice: + +```shell +> ➜ dotnet new console -n my-containerized-app +The template "Console App" was created successfully. + +Processing post-creation actions... +Restoring C:\Users\chethusk\OSS\Scratch\my-containerized-app\my-containerized-app.csproj: + Determining projects to restore... + Restored C:\Users\chethusk\OSS\Scratch\my-containerized-app\my-containerized-app.csproj (in 90 ms). +Restore succeeded. +> cd my-containerized-app +``` + +now, publish that app for linux-x64: + +```shell +> dotnet publish --os linux --arch x64 -p:Version=1.2.3 +MSBuild version 17.3.0-preview-22329-01+77c72dd0f for .NET + Determining projects to restore... + Restored C:\Users\chethusk\OSS\Scratch\my-containerized-app\my-containerized-app.csproj (in 1.39 sec). + my-containerized-app -> C:\Users\chethusk\OSS\Scratch\my-containerized-app\bin\Debug\net7.0\linux-x64\my-containerized-app.dll + my-containerized-app -> C:\Users\chethusk\OSS\Scratch\my-containerized-app\bin\Debug\net7.0\linux-x64\publish\ +``` + +Now, containerize your app: + +```shell +> containerize .\bin\Debug\net7.0\linux-x64\publish\ --entrypoint /app/my-containerized-app --name my-containerized-app +Reading from http://localhost:5010/ +Reading manifest for dotnet/runtime:7.0 +Copying from C:\Users\chethusk\OSS\Scratch\my-containerized-app\bin\Debug\net7.0\linux-x64\publish\ to /app +Pushed localhost:5010/my-containerized-app:latest +latest: Pulling from my-containerized-app +461246efe0a7: Already exists +2b6a8a95a1cb: Already exists +4bb378e5a440: Already exists +39c4bd5820ec: Already exists +5c433ed2534f: Pull complete +Digest: sha256:d52472a3e0b77e3fae4cf3ffe2ab3a575260c8ffd28c2e9a38b81d070f77fdb9 +Status: Downloaded newer image for localhost:5010/my-containerized-app:latest +localhost:5010/my-containerized-app:latest +Loaded image into local Docker daemon. Use 'docker run --rm -it --name my-containerized-app localhost:5010/my-containerized-app:latest' to run the application. +``` + +The output of the containerize command tells you the docker command to run, so lets do that: + +```shell +> docker run --rm -it --name my-containerized-app localhost:5010/my-containerized-app:latest +Hello, World! +``` + +That's it! From 1ca6ea1f605c8c7b063ac80d07b790d4c730d2ea Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 19 Jul 2022 21:27:21 -0500 Subject: [PATCH 0012/1880] add mechanism to get/set Workingdir of the image configuration --- containerize/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/containerize/Program.cs b/containerize/Program.cs index 59a69163aced..2de6b9b57532 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -66,6 +66,7 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry Console.WriteLine($"Reading from {registry.BaseUri}"); Image x = await registry.GetImageManifest(baseName, baseTag); + x.WorkingDirectory = workingDir; Console.WriteLine($"Copying from {folder.FullName} to {workingDir}"); Layer l = Layer.FromDirectory(folder.FullName, workingDir); From a4514766c41ff809cabacfa933956e7a8777e448 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 28 Jul 2022 10:08:05 -0500 Subject: [PATCH 0013/1880] Extract PushToLocalDockerViaRegistry --- containerize/Program.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index 2de6b9b57532..52636a67babe 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -75,6 +75,13 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry x.SetEntrypoint(entrypoint); + await PushToLocalDockerViaRegistry(registryName, baseName, imageName, registry, x); + + Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); +} + +static async Task PushToLocalDockerViaRegistry(string registryName, string baseName, string imageName, Registry registry, Image x) +{ // Push the image back to the local registry await registry.Push(x, imageName, baseName); @@ -83,7 +90,4 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry var pullBase = System.Diagnostics.Process.Start("docker", $"pull {registryName}/{imageName}:latest"); await pullBase.WaitForExitAsync(); - - Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); - } \ No newline at end of file From 982187bb8e608ffb24cf60b30cd87b6fed92e80c Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Fri, 29 Jul 2022 09:32:34 -0500 Subject: [PATCH 0014/1880] Make containerize write the image tarball --- containerize/Program.cs | 4 +++- containerize/Properties/launchSettings.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index 52636a67babe..a68f1b0ad591 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -75,7 +75,9 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry x.SetEntrypoint(entrypoint); - await PushToLocalDockerViaRegistry(registryName, baseName, imageName, registry, x); + //await PushToLocalDockerViaRegistry(registryName, baseName, imageName, registry, x); + using FileStream tarStream = new FileStream("test.tar", FileMode.OpenOrCreate); + await LocalDocker.WriteImageToStream(x, imageName, baseName, tarStream); Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); } diff --git a/containerize/Properties/launchSettings.json b/containerize/Properties/launchSettings.json index 444d22dc6574..cde620d6ba9e 100644 --- a/containerize/Properties/launchSettings.json +++ b/containerize/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "containerize": { "commandName": "Project", - "commandLineArgs": "--folder S:\\play\\helloworld6\\bin\\Debug\\net6.0\\publish --registry \"http://localhost:5010\" --base \"dotnet/sdk\" --baseTag \"6.0\" --entrypoint \"/app/helloworld6\" --name \"demo/container\"" + "commandLineArgs": "S:\\play\\container-demo\\bin\\Debug\\net6.0\\linux-x64\\ --registry localhost:5010 --base \"dotnet/runtime\" --baseTag \"6.0\" --entrypoint \"/app/container-demo\" --name \"showcase/demo\"" } } } \ No newline at end of file From 00b238d0759199a522e6afe63bbd8aaa5c8f74cb Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 1 Aug 2022 09:31:37 -0500 Subject: [PATCH 0015/1880] Extra debugging from `containerize` --- containerize/Program.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/containerize/Program.cs b/containerize/Program.cs index a68f1b0ad591..0970e0082474 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -1,5 +1,6 @@ using System.CommandLine; using System.Containers; +using System.Text.Json; var fileOption = new Argument( name: "folder", @@ -68,6 +69,14 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry Image x = await registry.GetImageManifest(baseName, baseTag); x.WorkingDirectory = workingDir; + JsonSerializerOptions options = new() + { + WriteIndented = true, + }; + + File.WriteAllTextAsync("manifest.json", x.manifest.ToJsonString(options)); + File.WriteAllTextAsync("config.json", x.config.ToJsonString(options)); + Console.WriteLine($"Copying from {folder.FullName} to {workingDir}"); Layer l = Layer.FromDirectory(folder.FullName, workingDir); From 98ca2dbd044e865e2775f7fe52517b8481576138 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 1 Aug 2022 13:25:55 -0500 Subject: [PATCH 0016/1880] LocalImage works for existing layers (but not added ones yet) --- containerize/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index 0970e0082474..b7b6e8a47efd 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -80,9 +80,9 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry Console.WriteLine($"Copying from {folder.FullName} to {workingDir}"); Layer l = Layer.FromDirectory(folder.FullName, workingDir); - x.AddLayer(l); + //x.AddLayer(l); - x.SetEntrypoint(entrypoint); + //x.SetEntrypoint(entrypoint); //await PushToLocalDockerViaRegistry(registryName, baseName, imageName, registry, x); using FileStream tarStream = new FileStream("test.tar", FileMode.OpenOrCreate); From a72efbfc61b29ca3a8499441fd90ceba44554a8c Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 1 Aug 2022 14:15:51 -0500 Subject: [PATCH 0017/1880] Local tarball save/load works --- containerize/Program.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index b7b6e8a47efd..9f7ab20129f6 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -74,17 +74,18 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry WriteIndented = true, }; - File.WriteAllTextAsync("manifest.json", x.manifest.ToJsonString(options)); - File.WriteAllTextAsync("config.json", x.config.ToJsonString(options)); - Console.WriteLine($"Copying from {folder.FullName} to {workingDir}"); Layer l = Layer.FromDirectory(folder.FullName, workingDir); - //x.AddLayer(l); + x.AddLayer(l); - //x.SetEntrypoint(entrypoint); + x.SetEntrypoint(entrypoint); + + File.WriteAllTextAsync("manifest.json", x.manifest.ToJsonString(options)); + File.WriteAllTextAsync("config.json", x.config.ToJsonString(options)); //await PushToLocalDockerViaRegistry(registryName, baseName, imageName, registry, x); + using FileStream tarStream = new FileStream("test.tar", FileMode.OpenOrCreate); await LocalDocker.WriteImageToStream(x, imageName, baseName, tarStream); From a3d8af0cc65d961ab437c11508e6cc8d5df43b01 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 1 Aug 2022 14:48:41 -0500 Subject: [PATCH 0018/1880] Use local docker load with stdin --- containerize/Program.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index 9f7ab20129f6..3c4b2799285c 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -81,13 +81,10 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry x.SetEntrypoint(entrypoint); - File.WriteAllTextAsync("manifest.json", x.manifest.ToJsonString(options)); - File.WriteAllTextAsync("config.json", x.config.ToJsonString(options)); + // File.WriteAllTextAsync("manifest.json", x.manifest.ToJsonString(options)); + // File.WriteAllTextAsync("config.json", x.config.ToJsonString(options)); - //await PushToLocalDockerViaRegistry(registryName, baseName, imageName, registry, x); - - using FileStream tarStream = new FileStream("test.tar", FileMode.OpenOrCreate); - await LocalDocker.WriteImageToStream(x, imageName, baseName, tarStream); + await LocalDocker.Load(x, imageName, baseName); Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); } From 7a9c94ed945201cdf764bbc2bcc60411da51b336 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 5 Aug 2022 10:40:53 -0500 Subject: [PATCH 0019/1880] add a quick markdown of the steps to go from zero to running for hte current verison --- docs/INTEGRATED-DEMO.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/INTEGRATED-DEMO.md diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md new file mode 100644 index 000000000000..8a4342296a9d --- /dev/null +++ b/docs/INTEGRATED-DEMO.md @@ -0,0 +1,40 @@ +# Using the generated package to do an end-to-end container build + +This guidance will track the most up-to-date version of the package and tasks. +You should expect it to shrink noticeably over time! + +```bash +# Prerequisite - have a local container registry running on port 5010. +# This will go away shortly in favor of pushing to your local docker +# daemon by default +docker run -d -p 5010:5000 --restart=always --name registry registry:2 + +# create a new project and move to its directory +dotnet new console -n my-awesome-container-app +cd my-awesome-container-app + +# create a nuget.config file to store the configuration for this repo +dotnet new nugetconfig + +# add a source for a github nuget registry, so that we can install the package. +# this relies on the GITHUB_USERNAME and +# GITHUB_TOKEN environment variables being present, the token should have 'read:packages' +# permissions. (replace the \ with ` if using powershell) +dotnet nuget add source https://nuget.pkg.github.com/rainersigwald/index.json \ + --name rainer --username '%GITHUB_USERNAME%' --password '%GITHUB_TOKEN%' \ + --store-password-in-clear-text --configfile nuget.config + +# add a reference to the package +dotnet add package System.Containers.Tasks --version 0.1.0 + +# publish your project +dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer ` + -p:ContainerBaseImageName=dotnet/runtime ` + -p:ContainerBaseImageTag=7.0 ` + -p:ContainerEntryPoint="dotnet" + -p:ContainerEntryPointArgs="/app/my-awesome-container-app.dll" + +# run your app +docker run -it --rm localhost:5010/my-awesome-container-app +``` + From b1d4529ee68f140d1f7826d2cec4b8dfe6bd227a Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 5 Aug 2022 16:53:34 -0500 Subject: [PATCH 0020/1880] update guidance to get a lot simpler --- docs/INTEGRATED-DEMO.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md index 8a4342296a9d..26a1f09ffffa 100644 --- a/docs/INTEGRATED-DEMO.md +++ b/docs/INTEGRATED-DEMO.md @@ -4,13 +4,8 @@ This guidance will track the most up-to-date version of the package and tasks. You should expect it to shrink noticeably over time! ```bash -# Prerequisite - have a local container registry running on port 5010. -# This will go away shortly in favor of pushing to your local docker -# daemon by default -docker run -d -p 5010:5000 --restart=always --name registry registry:2 - # create a new project and move to its directory -dotnet new console -n my-awesome-container-app +dotnet new web -n my-awesome-container-app cd my-awesome-container-app # create a nuget.config file to store the configuration for this repo @@ -25,16 +20,12 @@ dotnet nuget add source https://nuget.pkg.github.com/rainersigwald/index.json \ --store-password-in-clear-text --configfile nuget.config # add a reference to the package -dotnet add package System.Containers.Tasks --version 0.1.0 +dotnet add package System.Containers.Tasks --version 0.1.3 # publish your project -dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer ` - -p:ContainerBaseImageName=dotnet/runtime ` - -p:ContainerBaseImageTag=7.0 ` - -p:ContainerEntryPoint="dotnet" - -p:ContainerEntryPointArgs="/app/my-awesome-container-app.dll" +dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer # run your app -docker run -it --rm localhost:5010/my-awesome-container-app +docker run -it --rm my-awesome-container-app:latest ``` From 35fbb3b0f6d541d740876fa69a6e6c4b12b887e7 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 8 Aug 2022 12:29:48 -0500 Subject: [PATCH 0021/1880] Rename to Microsoft.NET.Build.Containers --- .../ContainerHelpers.cs | 90 +++++++ .../ContentStore.cs | 56 +++++ Microsoft.NET.Build.Containers/Descriptor.cs | 68 ++++++ Microsoft.NET.Build.Containers/Image.cs | 108 +++++++++ Microsoft.NET.Build.Containers/Layer.cs | 86 +++++++ Microsoft.NET.Build.Containers/LocalDocker.cs | 93 ++++++++ .../Microsoft.NET.Build.Containers.csproj | 9 + Microsoft.NET.Build.Containers/Registry.cs | 224 ++++++++++++++++++ containerize/Program.cs | 2 +- containerize/containerize.csproj | 2 +- docs/INTEGRATED-DEMO.md | 2 +- 11 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 Microsoft.NET.Build.Containers/ContainerHelpers.cs create mode 100644 Microsoft.NET.Build.Containers/ContentStore.cs create mode 100644 Microsoft.NET.Build.Containers/Descriptor.cs create mode 100644 Microsoft.NET.Build.Containers/Image.cs create mode 100644 Microsoft.NET.Build.Containers/Layer.cs create mode 100644 Microsoft.NET.Build.Containers/LocalDocker.cs create mode 100644 Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj create mode 100644 Microsoft.NET.Build.Containers/Registry.cs diff --git a/Microsoft.NET.Build.Containers/ContainerHelpers.cs b/Microsoft.NET.Build.Containers/ContainerHelpers.cs new file mode 100644 index 000000000000..a8f8cc624721 --- /dev/null +++ b/Microsoft.NET.Build.Containers/ContainerHelpers.cs @@ -0,0 +1,90 @@ +namespace Microsoft.NET.Build.Containers; + +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +public static class ContainerHelpers +{ + private static Regex imageTagRegex = new Regex("^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$"); + + private static Regex imageNameRegex = new Regex("^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*$"); + + /// + /// Given some "fully qualified" image name (e.g. mcr.microsoft.com/dotnet/runtime), return + /// a valid UriBuilder. This means appending 'https' if the URI is not absolute, otherwise UriBuilder will throw. + /// + /// + /// A with the given containerBase, or, if containerBase is relative, https:// + containerBase + private static Uri? ContainerImageToUri(string containerBase) + { + Uri uri = new Uri(containerBase, UriKind.RelativeOrAbsolute); + + try + { + return uri.IsAbsoluteUri ? uri : new Uri(containerBase.Contains("localhost") ? "http://" : "https://" + uri); + } + catch (Exception e) + { + Console.WriteLine("Failed parsing the container image into a UriBuilder: {0}", e); + return null; + } + } + + /// + /// Ensures the given image name is valid. + /// Spec: https://github.com/opencontainers/distribution-spec/blob/4ab4752c3b86a926d7e5da84de64cbbdcc18d313/spec.md#pulling-manifests + /// + /// + /// + public static bool IsValidImageName(string imageName) + { + return imageNameRegex.IsMatch(imageName); + } + + /// + /// Ensures the given tag is valid. + /// Spec: https://github.com/opencontainers/distribution-spec/blob/4ab4752c3b86a926d7e5da84de64cbbdcc18d313/spec.md#pulling-manifests + /// + /// + /// + public static bool IsValidImageTag(string imageTag) + { + return imageTagRegex.IsMatch(imageTag); + } + + /// + /// Parse a fully qualified container name (e.g. https://mcr.microsoft.com/dotnet/runtime:6.0) + /// Note: Tag not required. + /// + /// + /// + /// + /// + /// True if the parse was successful. When false is returned, all out vars are set to empty strings. + public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedContainerName, + [NotNullWhen(true)] out string? containerRegistry, + [NotNullWhen(true)] out string? containerName, + [NotNullWhen(true)] out string? containerTag) + { + Uri? uri = ContainerImageToUri(fullyQualifiedContainerName); + + if (uri == null || uri.Segments.Length <= 1) + { + containerRegistry = null; + containerName = null; + containerTag = null; + return false; + } + + // The first segment is the '/', create a string out of everything after. + string image = uri.PathAndQuery.Substring(1); + + // If the image has a ':', there's a tag we need to parse. + int indexOfColon = image.IndexOf(':'); + + containerRegistry = uri.Scheme + "://" + uri.Host; + containerName = indexOfColon == -1 ? image : image.Substring(0, indexOfColon); + containerTag = indexOfColon == -1 ? "" : image.Substring(indexOfColon + 1); + return true; + } +} diff --git a/Microsoft.NET.Build.Containers/ContentStore.cs b/Microsoft.NET.Build.Containers/ContentStore.cs new file mode 100644 index 000000000000..9f71ca21cf0b --- /dev/null +++ b/Microsoft.NET.Build.Containers/ContentStore.cs @@ -0,0 +1,56 @@ +using System.Diagnostics; + +namespace Microsoft.NET.Build.Containers; + +public static class ContentStore +{ + public static string ArtifactRoot { get; set; } = Path.Combine(Path.GetTempPath(), "Containers"); + public static string ContentRoot + { + get => Path.Combine(ArtifactRoot, "Content"); + } + + public static string TempPath + { + get + { + string tempPath = Path.Join(ArtifactRoot, "Temp"); + + Directory.CreateDirectory(tempPath); + + return tempPath; + } + } + + public static string PathForDescriptor(Descriptor descriptor) + { + string digest = descriptor.Digest; + + Debug.Assert(digest.StartsWith("sha256:")); + + string contentHash = digest.Substring("sha256:".Length); + + string extension = descriptor.MediaType switch + { + "application/vnd.docker.image.rootfs.diff.tar.gzip" + or "application/vnd.docker.image.rootfs.diff.tar" + or "application/vnd.oci.image.layer.v1.tar" + or "application/vnd.oci.image.layer.v1.tar+gzip" + => ".tar", + _ => throw new ArgumentException($"Unrecognized mediaType '{descriptor.MediaType}'") + }; + + return GetPathForHash(contentHash) + extension; + } + + + public static string GetPathForHash(string contentHash) + { + return Path.Combine(ContentRoot, contentHash); + } + + public static string GetTempFile() + { + return Path.Join(TempPath, Path.GetRandomFileName()); + } +} diff --git a/Microsoft.NET.Build.Containers/Descriptor.cs b/Microsoft.NET.Build.Containers/Descriptor.cs new file mode 100644 index 000000000000..9ccfbc4a1103 --- /dev/null +++ b/Microsoft.NET.Build.Containers/Descriptor.cs @@ -0,0 +1,68 @@ +using System.Text.Json.Serialization; + +namespace Microsoft.NET.Build.Containers; + +/// +/// An OCI Content Descriptor describing a component. +/// +/// +/// . +/// +public readonly record struct Descriptor +{ + /// + /// Media type of the referenced content. + /// + /// + /// Likely to be an OCI media type defined at . + /// + // TODO: validate against RFC 6838 naming conventions? + [JsonPropertyName("mediaType")] + public string MediaType { get; init; } + + /// + /// Digest of the content, specifying algorithm and value. + /// + /// + /// + /// + [JsonPropertyName("digest")] + public string Digest { get; init; } + + /// + /// Size, in bytes, of the raw content. + /// + [JsonPropertyName("size")] + public long Size { get; init; } + + /// + /// Optional list of URLs where the content may be downloaded. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? Urls { get; init; } = null; + + /// + /// Arbitrary metadata for this descriptor. + /// + /// + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Annotations { get; init; } = null; + + /// + /// Embedded representation of the referenced content, base-64 encoded. + /// + /// + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Data { get; init; } = null; + + public Descriptor(string mediaType, string digest, long size) + { + MediaType = mediaType; + Digest = digest; + Size = size; + } +} diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs new file mode 100644 index 000000000000..8e3a77f956da --- /dev/null +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -0,0 +1,108 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.NET.Build.Containers; + +public class Image +{ + public JsonNode manifest; + public JsonNode config; + + public readonly string OriginatingName; + internal readonly Registry? originatingRegistry; + + internal List newLayers = new(); + + public Image(JsonNode manifest, JsonNode config, string name, Registry? registry) + { + this.manifest = manifest; + this.config = config; + this.OriginatingName = name; + this.originatingRegistry = registry; + } + + public IEnumerable LayerDescriptors + { + get + { + JsonNode? layersNode = manifest["layers"]; + + if (layersNode is null) + { + throw new NotImplementedException("Tried to get layer information but there is no layer node?"); + } + + foreach (JsonNode? descriptorJson in layersNode.AsArray()) + { + if (descriptorJson is null) + { + throw new NotImplementedException("Null layer descriptor in the list?"); + } + + yield return descriptorJson.Deserialize(); + } + } + } + + public void AddLayer(Layer l) + { + newLayers.Add(l); + manifest["layers"]!.AsArray().Add(l.Descriptor); + config["rootfs"]!["diff_ids"]!.AsArray().Add(l.Descriptor.Digest); // TODO: this should be the descriptor of the UNCOMPRESSED tarball (once we turn on compression) + RecalculateDigest(); + } + + private void RecalculateDigest() { + manifest["config"]!["digest"] = GetDigest(config); + } + + public void SetEntrypoint(string executable, string[]? args = null) + { + JsonObject? configObject = config["config"]!.AsObject(); + + if (configObject is null) + { + throw new NotImplementedException("Expected base image to have a config node"); + } + + configObject["Entrypoint"] = executable; + + if (args is null) + { + configObject.Remove("Cmd"); + } + else + { + configObject["Cmd"] = new JsonArray(args.Where(s => !string.IsNullOrEmpty(s)).Select(s =>(JsonObject)s).ToArray()); + } + + RecalculateDigest(); + } + + public string WorkingDirectory { + get => (string?)manifest["config"]!["WorkingDir"] ?? ""; + set { + config["config"]!["WorkingDir"] = value; + RecalculateDigest(); + } + } + + public string GetDigest(JsonNode json) + { + string hashString; + + hashString = GetSha(json); + + return $"sha256:{hashString}"; + } + + public static string GetSha(JsonNode json) + { + using SHA256 mySHA256 = SHA256.Create(); + byte[] hash = mySHA256.ComputeHash(Encoding.UTF8.GetBytes(json.ToJsonString())); + + return Convert.ToHexString(hash).ToLowerInvariant(); + } +} diff --git a/Microsoft.NET.Build.Containers/Layer.cs b/Microsoft.NET.Build.Containers/Layer.cs new file mode 100644 index 000000000000..62cfe08c5d04 --- /dev/null +++ b/Microsoft.NET.Build.Containers/Layer.cs @@ -0,0 +1,86 @@ +using System.Formats.Tar; +using System.Security.Cryptography; + +namespace Microsoft.NET.Build.Containers; + +public record struct Layer +{ + public Descriptor Descriptor { get; private set; } + + public string BackingFile { get; private set; } + + public static Layer FromDirectory(string directory, string containerPath) + { + DirectoryInfo di = new(directory); + + IEnumerable<(string path, string containerPath)> fileList = + di.GetFileSystemInfos() + .Where(fsi => fsi is FileInfo).Select( + fsi => + { + string destinationPath = + Path.Join(containerPath, + Path.GetRelativePath(directory, fsi.FullName)) + .Replace(Path.DirectorySeparatorChar, '/'); + return (fsi.FullName, destinationPath); + }); + + return FromFiles(fileList); + } + + public static Layer FromFiles(IEnumerable<(string path, string containerPath)> fileList) + { + long fileSize; + byte[] hash; + + string tempTarballPath = ContentStore.GetTempFile(); + using (FileStream fs = File.Create(tempTarballPath)) + { + // using (GZipStream gz = new(fs, CompressionMode.Compress)) // TODO: https://github.com/rainersigwald/containers/issues/29 + using (TarWriter writer = new(fs, TarEntryFormat.Gnu, leaveOpen: true)) + { + foreach (var item in fileList) + { + // Docker treats a COPY instruction that copies to a path like `/app` by + // including `app/` as a directory, with no leading slash. Emulate that here. + string containerPath = item.containerPath.TrimStart(PathSeparators); + + writer.WriteEntry(item.path, containerPath); + } + } + + fileSize = fs.Length; + + fs.Position = 0; + + using SHA256 mySHA256 = SHA256.Create(); + hash = mySHA256.ComputeHash(fs); + } + + string contentHash = Convert.ToHexString(hash).ToLowerInvariant(); + + Descriptor descriptor = new() + { + MediaType = "application/vnd.docker.image.rootfs.diff.tar", // TODO: configurable? gzip always? + Size = fileSize, + Digest = $"sha256:{contentHash}" + }; + + string storedContent = ContentStore.PathForDescriptor(descriptor); + + Directory.CreateDirectory(ContentStore.ContentRoot); + + File.Move(tempTarballPath, storedContent, overwrite: true); + + Layer l = new() + { + Descriptor = descriptor, + BackingFile = storedContent, + }; + + return l; + } + + private readonly static char[] PathSeparators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + +} \ No newline at end of file diff --git a/Microsoft.NET.Build.Containers/LocalDocker.cs b/Microsoft.NET.Build.Containers/LocalDocker.cs new file mode 100644 index 000000000000..bacfdf06c112 --- /dev/null +++ b/Microsoft.NET.Build.Containers/LocalDocker.cs @@ -0,0 +1,93 @@ +using System.Diagnostics; +using System.Formats.Tar; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.NET.Build.Containers; + +public class LocalDocker +{ + public static async Task Load(Image x, string name, string baseName) + { + // call `docker load` and get it ready to recieve input + ProcessStartInfo loadInfo = new("docker", $"load"); + loadInfo.RedirectStandardInput = true; + loadInfo.RedirectStandardOutput = true; + + using Process? loadProcess = Process.Start(loadInfo); + + if (loadProcess is null) + { + throw new NotImplementedException("Failed creating docker process"); + } + + // Create new stream tarball + + await WriteImageToStream(x, name, loadProcess.StandardInput.BaseStream); + + loadProcess.StandardInput.Close(); + + await loadProcess.WaitForExitAsync(); + } + + public static async Task WriteImageToStream(Image x, string name, Stream imageStream) + { + TarWriter writer = new(imageStream, TarEntryFormat.Gnu, leaveOpen: true); + + + // Feed each layer tarball into the stream + JsonArray layerTarballPaths = new JsonArray(); + + foreach (var d in x.LayerDescriptors) + { + if (!x.originatingRegistry.HasValue) + { + throw new NotImplementedException("Need a good error for 'couldn't download a thing because no link to registry'"); + } + + string localPath = await x.originatingRegistry.Value.DownloadBlob(x.OriginatingName, d); + + // Stuff that (uncompressed) tarball into the image tar stream + string layerTarballPath = $"{d.Digest.Substring("sha256:".Length)}/layer.tar"; + writer.WriteEntry(localPath, layerTarballPath); + layerTarballPaths.Add(layerTarballPath); + } + + // add config + string configTarballPath = $"{Image.GetSha(x.config)}.json"; + + using (MemoryStream configStream = new MemoryStream(Encoding.UTF8.GetBytes(x.config.ToJsonString()))) + { + GnuTarEntry configEntry = new(TarEntryType.RegularFile, configTarballPath) + { + DataStream = configStream + }; + + writer.WriteEntry(configEntry); // TODO: asyncify these when API available (Preview 7) + } + + // Add manifest + JsonArray tagsNode = new() + { + name + ":latest" // TODO: do something else here? + }; + + JsonNode manifestNode = new JsonArray(new JsonObject + { + { "Config", configTarballPath }, + { "RepoTags", tagsNode }, + { "Layers", layerTarballPaths } + }); + + using (MemoryStream manifestStream = new MemoryStream(Encoding.UTF8.GetBytes(manifestNode.ToJsonString()))) + { + GnuTarEntry manifestEntry = new(TarEntryType.RegularFile, "manifest.json") + { + DataStream = manifestStream + }; + + writer.WriteEntry(manifestEntry); // TODO: asyncify these when API available (Preview 7) + } + } +} diff --git a/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj new file mode 100644 index 000000000000..cfadb03dd5ae --- /dev/null +++ b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs new file mode 100644 index 000000000000..c2d34937f792 --- /dev/null +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -0,0 +1,224 @@ +using Microsoft.VisualBasic; + +using System.Diagnostics; +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Xml.Linq; + +namespace Microsoft.NET.Build.Containers; + +public record struct Registry(Uri BaseUri) +{ + private const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json"; + private const string DockerContainerV1 = "application/vnd.docker.container.image.v1+json"; + + public async Task GetImageManifest(string name, string reference) + { + using HttpClient client = GetClient(); + + var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{reference}")); + + response.EnsureSuccessStatusCode(); + + var s = await response.Content.ReadAsStringAsync(); + + var manifest = JsonNode.Parse(s); + + if (manifest is null) throw new NotImplementedException("Got a manifest but it was null"); + + if ((string?)manifest["mediaType"] != DockerManifestV2) + { + throw new NotImplementedException($"Do not understand the mediaType {manifest["mediaType"]}"); + } + + JsonNode? config = manifest["config"]; + Debug.Assert(config is not null); + Debug.Assert(((string?)config["mediaType"]) == DockerContainerV1); + + string? configSha = (string?)config["digest"]; + Debug.Assert(configSha is not null); + + response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/blobs/{configSha}")); + + JsonNode? configDoc = JsonNode.Parse(await response.Content.ReadAsStringAsync()); + Debug.Assert(configDoc is not null); + //Debug.Assert(((string?)configDoc["mediaType"]) == DockerContainerV1); + + return new Image(manifest, configDoc, name, this); + } + + /// + /// Ensure a blob associated with from the registry is available locally. + /// + /// Name of the associated image. + /// that describes the blob. + /// Local path to the (decompressed) blob content. + public async Task DownloadBlob(string name, Descriptor descriptor) + { + string localPath = ContentStore.PathForDescriptor(descriptor); + + if (File.Exists(localPath)) + { + // Assume file is up to date and just return it + return localPath; + } + + // No local copy, so download one + + using HttpClient client = GetClient(); + + var response = await client.GetAsync(new Uri(BaseUri, $"/v2/{name}/blobs/{descriptor.Digest}")); + + response.EnsureSuccessStatusCode(); + + string tempTarballPath = ContentStore.GetTempFile(); + using (FileStream fs = File.Create(tempTarballPath)) + { + Stream? gzs = null; + + Stream responseStream = await response.Content.ReadAsStreamAsync(); + if (descriptor.MediaType.EndsWith("gzip")) + { + gzs = new GZipStream(responseStream, CompressionMode.Decompress); + } + + using Stream? gzipStreamToDispose = gzs; + + await (gzs ?? responseStream).CopyToAsync(fs); + } + + File.Move(tempTarballPath, localPath, overwrite: true); + + return localPath; + } + + public async Task Push(Layer layer, string name) + { + string digest = layer.Descriptor.Digest; + + using (FileStream contents = File.OpenRead(layer.BackingFile)) + { + await UploadBlob(name, digest, contents); + } + } + + private readonly async Task UploadBlob(string name, string digest, Stream contents) + { + using HttpClient client = GetClient(); + + if (await BlobAlreadyUploaded(name, digest, client)) + { + // Already there! + return; + } + + HttpResponseMessage pushResponse = await client.PostAsync(new Uri(BaseUri, $"/v2/{name}/blobs/uploads/"), content: null); + + Debug.Assert(pushResponse.StatusCode == HttpStatusCode.Accepted); + + //Uri uploadUri = new(BaseUri, pushResponse.Headers.GetValues("location").Single() + $"?digest={layer.Descriptor.Digest}"); + Debug.Assert(pushResponse.Headers.Location is not null); + + var x = new UriBuilder(pushResponse.Headers.Location); + + x.Query += $"&digest={Uri.EscapeDataString(digest)}"; + + // TODO: consider chunking + StreamContent content = new StreamContent(contents); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + content.Headers.ContentLength = contents.Length; + HttpResponseMessage putResponse = await client.PutAsync(x.Uri, content); + + string resp = await putResponse.Content.ReadAsStringAsync(); + + putResponse.EnsureSuccessStatusCode(); + } + + private readonly async Task BlobAlreadyUploaded(string name, string digest, HttpClient client) + { + HttpResponseMessage response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, new Uri(BaseUri, $"/v2/{name}/blobs/{digest}"))); + + if (response.StatusCode == HttpStatusCode.OK) + { + return true; + } + + return false; + } + + private static HttpClient GetClient() + { + HttpClient client = new(new HttpClientHandler() { UseDefaultCredentials = true }); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue(DockerManifestV2)); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue(DockerContainerV1)); + + client.DefaultRequestHeaders.Add("User-Agent", ".NET Container Library"); + + return client; + } + + public async Task Push(Image x, string name, string baseName) + { + using HttpClient client = GetClient(); + + foreach (var descriptor in x.LayerDescriptors) + { + string digest = descriptor.Digest; + + if (await BlobAlreadyUploaded(name, digest, client)) + { + continue; + } + + // Blob wasn't there; can we tell the server to get it from the base image? + HttpResponseMessage pushResponse = await client.PostAsync(new Uri(BaseUri, $"/v2/{name}/blobs/uploads/?mount={digest}&from={baseName}"), content: null); + + if (pushResponse.StatusCode != HttpStatusCode.Created) + { + // The blob wasn't already available in another namespace, so fall back to explicitly uploading it + + // TODO: don't do this search, which is ridiculous + foreach (Layer layer in x.newLayers) + { + if (layer.Descriptor.Digest == digest) + { + await Push(layer, name); + break; + } + + throw new NotImplementedException("Need to push a layer but it's not a new one--need to download it from the base registry and upload it"); + } + } + } + + using (MemoryStream stringStream = new MemoryStream(Encoding.UTF8.GetBytes(x.config.ToJsonString()))) + { + await UploadBlob(name, x.GetDigest(x.config), stringStream); + } + + HttpContent manifestUploadContent = new StringContent(x.manifest.ToJsonString()); + manifestUploadContent.Headers.ContentType = new MediaTypeHeaderValue(DockerManifestV2); + + var putResponse = await client.PutAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{x.GetDigest(x.manifest)}"), manifestUploadContent); + + string putresponsestr = await putResponse.Content.ReadAsStringAsync(); + + putResponse.EnsureSuccessStatusCode(); + + var putResponse2 = await client.PutAsync(new Uri(BaseUri, $"/v2/{name}/manifests/latest"), manifestUploadContent); + + putResponse2.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/containerize/Program.cs b/containerize/Program.cs index 3c4b2799285c..2a449cd1ce6c 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -1,5 +1,5 @@ using System.CommandLine; -using System.Containers; +using Microsoft.NET.Build.Containers; using System.Text.Json; var fileOption = new Argument( diff --git a/containerize/containerize.csproj b/containerize/containerize.csproj index c541d4e80d9e..25728dadafed 100644 --- a/containerize/containerize.csproj +++ b/containerize/containerize.csproj @@ -12,7 +12,7 @@ - + diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md index 26a1f09ffffa..cae324c12a38 100644 --- a/docs/INTEGRATED-DEMO.md +++ b/docs/INTEGRATED-DEMO.md @@ -20,7 +20,7 @@ dotnet nuget add source https://nuget.pkg.github.com/rainersigwald/index.json \ --store-password-in-clear-text --configfile nuget.config # add a reference to the package -dotnet add package System.Containers.Tasks --version 0.1.3 +dotnet add package Microsoft.NET.Build.Containers.Tasks --version 0.1.3 # publish your project dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer From 26eb0ee66b0ba035f9ea77ec45304bf5b571bcf0 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 8 Aug 2022 15:52:23 -0500 Subject: [PATCH 0022/1880] Consolidate tasks into primary assembly (#75) This is smaller and easier to deal with. --- .../CreateNewImage.cs | 142 ++++++++++++++++++ .../Microsoft.NET.Build.Containers.csproj | 51 +++++++ .../ParseContainerProperties.cs | 110 ++++++++++++++ .../Microsoft.NET.Build.Containers.props | 14 ++ .../Microsoft.NET.Build.Containers.targets | 71 +++++++++ docs/INTEGRATED-DEMO.md | 2 +- 6 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 Microsoft.NET.Build.Containers/CreateNewImage.cs create mode 100644 Microsoft.NET.Build.Containers/ParseContainerProperties.cs create mode 100644 Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props create mode 100644 Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs new file mode 100644 index 000000000000..ca87bd79e801 --- /dev/null +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -0,0 +1,142 @@ +using Microsoft.Build.Framework; + +namespace Microsoft.NET.Build.Containers.Tasks; + +public class CreateNewImage : Microsoft.Build.Utilities.Task +{ + /// + /// The base registry to pull from. + /// Ex: https://mcr.microsoft.com + /// + [Required] + public string BaseRegistry { get; set; } + + /// + /// The base image to pull. + /// Ex: dotnet/runtime + /// + [Required] + public string BaseImageName { get; set; } + + /// + /// The base image tag. + /// Ex: 6.0 + /// + [Required] + public string BaseImageTag { get; set; } + + /// + /// The registry to push to. + /// + [Required] + public string OutputRegistry { get; set; } + + /// + /// The name of the output image that will be pushed to the registry. + /// + [Required] + public string ImageName { get; set; } + + /// + /// The tag to associate with the new image. + /// + public string ImageTag { get; set; } + + /// + /// The directory for the build outputs to be published. + /// Constructed from "$(MSBuildProjectDirectory)\$(PublishDir)" + /// + [Required] + public string PublishDirectory { get; set; } + + /// + /// The working directory of the container. + /// + [Required] + public string WorkingDirectory { get; set; } + + /// + /// The entrypoint application of the container. + /// + [Required] + public string Entrypoint { get; set; } + + /// + /// Arguments to pass alongside Entrypoint. + /// + public string EntrypointArgs { get; set; } + + public CreateNewImage() + { + BaseRegistry = ""; + BaseImageName = ""; + BaseImageTag = ""; + OutputRegistry = ""; + ImageName = ""; + ImageTag = ""; + PublishDirectory = ""; + WorkingDirectory = ""; + Entrypoint = ""; + EntrypointArgs = ""; + } + + + public override bool Execute() + { + if (!Directory.Exists(PublishDirectory)) + { + Log.LogError("{0} '{1}' does not exist", nameof(PublishDirectory), PublishDirectory); + return !Log.HasLoggedErrors; + } + + Registry reg; + Image image; + + try + { + reg = new Registry(new Uri(BaseRegistry, UriKind.RelativeOrAbsolute)); + image = reg.GetImageManifest(BaseImageName, BaseImageTag).Result; + } + catch + { + throw; + } + + if (BuildEngine != null) + { + Log.LogMessage($"Loading from directory: {PublishDirectory}"); + } + + Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory); + image.AddLayer(newLayer); + image.SetEntrypoint(Entrypoint, EntrypointArgs?.Split(' ').ToArray()); + + if (OutputRegistry.StartsWith("docker://")) + { + LocalDocker.Load(image, ImageName, BaseImageName).Wait(); + } + else + { + Registry outputReg = new Registry(new Uri(OutputRegistry)); + try + { + outputReg.Push(image, ImageName, BaseImageName).Wait(); + } + catch (Exception e) + { + if (BuildEngine != null) + { + Log.LogError("Failed to push to the output registry: {0}", e); + } + return !Log.HasLoggedErrors; + } + } + + if (BuildEngine != null) + { + Log.LogMessage(MessageImportance.High, "Pushed container '{0}:{1}' to registry '{2}'", ImageName, ImageTag, OutputRegistry); + } + + return !Log.HasLoggedErrors; + } +} \ No newline at end of file diff --git a/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj index cfadb03dd5ae..a1472dcba1eb 100644 --- a/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj +++ b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj @@ -4,6 +4,57 @@ net7.0 enable enable + true + + true + + + $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage + + + tasks + + + true + + + NU5100;NU5128 + Rainer Sigwald, Ben Villalobos, Chet Husk + Microsoft + Tasks and targets to natively publish .NET applications as containers. + + MIT + https://github.com/rainersigwald/containers + https://github.com/rainersigwald/containers + git + containers;docker;Microsoft.NET.Build.Containers + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Microsoft.NET.Build.Containers/ParseContainerProperties.cs b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs new file mode 100644 index 000000000000..1e3e26bd20ff --- /dev/null +++ b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs @@ -0,0 +1,110 @@ +using Microsoft.Build.Framework; + +namespace Microsoft.NET.Build.Containers.Tasks; + +public class ParseContainerProperties : Microsoft.Build.Utilities.Task +{ + /// + /// The full base image name. mcr.microsoft.com/dotnet/runtime:6.0, for example. + /// + [Required] + public string FullyQualifiedBaseImageName { get; set; } + + /// + /// The registry to push the new container to. + /// + [Required] + public string ContainerRegistry { get; set; } + + /// + /// The image name for the container to be created. + /// + [Required] + public string ContainerImageName { get; set; } + + /// + /// The tag for the container to be created. + /// + [Required] + public string ContainerImageTag { get; set; } + + [Output] + public string ParsedContainerRegistry { get; private set; } + + [Output] + public string ParsedContainerImage { get; private set; } + + [Output] + public string ParsedContainerTag { get; private set; } + + [Output] + public string NewContainerRegistry { get; private set; } + + [Output] + public string NewContainerImageName { get; private set; } + + [Output] + public string NewContainerTag { get; private set; } + + public ParseContainerProperties() + { + FullyQualifiedBaseImageName = ""; + ContainerRegistry = ""; + ContainerImageName = ""; + ContainerImageTag = ""; + ParsedContainerRegistry = ""; + ParsedContainerImage = ""; + ParsedContainerTag = ""; + NewContainerRegistry = ""; + NewContainerImageName = ""; + NewContainerTag = ""; + } + + public override bool Execute() + { + if (!ContainerHelpers.IsValidImageName(ContainerImageName)) + { + Log.LogError($"Invalid {nameof(ContainerImageName)}: {0}", ContainerImageName); + return !Log.HasLoggedErrors; + } + + if (!string.IsNullOrEmpty(ContainerImageTag) && !ContainerHelpers.IsValidImageTag(ContainerImageTag)) + { + Log.LogError($"Invalid {nameof(ContainerImageTag)}: {0}", ContainerImageTag); + return !Log.HasLoggedErrors; + } + + if (FullyQualifiedBaseImageName.Contains(' ') && BuildEngine != null) + { + Log.LogWarning($"{nameof(FullyQualifiedBaseImageName)} had spaces in it, replacing with dashes."); + } + + if (!ContainerHelpers.TryParseFullyQualifiedContainerName(FullyQualifiedBaseImageName.Replace(' ', '-'), + out string? outputReg, + out string? outputImage, + out string? outputTag)) + { + Log.LogError($"Could not parse {nameof(FullyQualifiedBaseImageName)}: {0}", FullyQualifiedBaseImageName); + return !Log.HasLoggedErrors; + } + + ParsedContainerRegistry = outputReg; + ParsedContainerImage = outputImage; + ParsedContainerTag = outputTag; + NewContainerRegistry = ContainerRegistry; + NewContainerImageName = ContainerImageName; + NewContainerTag = ContainerImageTag; + + if (BuildEngine != null) + { + Log.LogMessage(MessageImportance.Low, "Parsed the following properties. Note: Spaces are replaced with dashes."); + Log.LogMessage(MessageImportance.Low, "Host: {0}", ParsedContainerRegistry); + Log.LogMessage(MessageImportance.Low, "Image: {0}", ParsedContainerImage); + Log.LogMessage(MessageImportance.Low, "Tag: {0}", ParsedContainerTag); + Log.LogMessage(MessageImportance.Low, "Image Name: {0}", NewContainerImageName); + Log.LogMessage(MessageImportance.Low, "Image Tag: {0}", NewContainerTag); + } + + return !Log.HasLoggedErrors; + } +} diff --git a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props new file mode 100644 index 000000000000..86181319e0e9 --- /dev/null +++ b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props @@ -0,0 +1,14 @@ + + + tasks + net7.0 + + $(MSBuildThisFileDirectory)..\$(taskForldername)\$(taskFramework) + + $(CustomTasksFolder)\$(MSBuildThisFileName).dll + + + + + + \ No newline at end of file diff --git a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets new file mode 100644 index 000000000000..75a7e3f97921 --- /dev/null +++ b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets @@ -0,0 +1,71 @@ + + + + + <_IsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true + <_ContainerBaseRegistry>https://mcr.microsoft.com + <_ContainerBaseImageName Condition="'$(_IsSelfContained)' == 'true'">dotnet/runtime-deps + <_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == '' and @(ProjectCapability->AnyHaveMetadataValue('Identity', 'AspNetCore'))">dotnet/aspnet + <_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == ''">dotnet/runtime + <_ContainerBaseImageTag>$(_TargetFrameworkVersionWithoutV) + + + + + $(_ContainerBaseRegistry)/$(_ContainerBaseImageName):$(_ContainerBaseImageTag) + docker:// + + $(AssemblyName) + $(Version) + /app + dotnet $(TargetFileName) + $(ContainerWorkingDirectory)/$(AssemblyName)$(_NativeExecutableExtension) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ComputeContainerConfig + + + + + + + \ No newline at end of file diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md index cae324c12a38..53a2955d4aba 100644 --- a/docs/INTEGRATED-DEMO.md +++ b/docs/INTEGRATED-DEMO.md @@ -20,7 +20,7 @@ dotnet nuget add source https://nuget.pkg.github.com/rainersigwald/index.json \ --store-password-in-clear-text --configfile nuget.config # add a reference to the package -dotnet add package Microsoft.NET.Build.Containers.Tasks --version 0.1.3 +dotnet add package Microsoft.NET.Build.Containers --version 0.1.3 # publish your project dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer From 56fe1e18ff6e53797b9a66236564cbef87b41087 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Fri, 5 Aug 2022 15:12:20 -0500 Subject: [PATCH 0023/1880] Support passing tag Instead of hardcoding "latest" everywhere. --- Microsoft.NET.Build.Containers/CreateNewImage.cs | 4 ++-- Microsoft.NET.Build.Containers/LocalDocker.cs | 8 ++++---- Microsoft.NET.Build.Containers/Registry.cs | 6 ++++-- containerize/Program.cs | 12 ++++++------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index ca87bd79e801..68d67807ce4c 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -113,14 +113,14 @@ public override bool Execute() if (OutputRegistry.StartsWith("docker://")) { - LocalDocker.Load(image, ImageName, BaseImageName).Wait(); + LocalDocker.Load(image, ImageName, ImageTag, BaseImageName).Wait(); } else { Registry outputReg = new Registry(new Uri(OutputRegistry)); try { - outputReg.Push(image, ImageName, BaseImageName).Wait(); + outputReg.Push(image, ImageName, ImageTag, BaseImageName).Wait(); } catch (Exception e) { diff --git a/Microsoft.NET.Build.Containers/LocalDocker.cs b/Microsoft.NET.Build.Containers/LocalDocker.cs index bacfdf06c112..b6430bfd9362 100644 --- a/Microsoft.NET.Build.Containers/LocalDocker.cs +++ b/Microsoft.NET.Build.Containers/LocalDocker.cs @@ -8,7 +8,7 @@ namespace Microsoft.NET.Build.Containers; public class LocalDocker { - public static async Task Load(Image x, string name, string baseName) + public static async Task Load(Image x, string name, string tag, string baseName) { // call `docker load` and get it ready to recieve input ProcessStartInfo loadInfo = new("docker", $"load"); @@ -24,14 +24,14 @@ public static async Task Load(Image x, string name, string baseName) // Create new stream tarball - await WriteImageToStream(x, name, loadProcess.StandardInput.BaseStream); + await WriteImageToStream(x, name, tag, loadProcess.StandardInput.BaseStream); loadProcess.StandardInput.Close(); await loadProcess.WaitForExitAsync(); } - public static async Task WriteImageToStream(Image x, string name, Stream imageStream) + public static async Task WriteImageToStream(Image x, string name, string tag, Stream imageStream) { TarWriter writer = new(imageStream, TarEntryFormat.Gnu, leaveOpen: true); @@ -70,7 +70,7 @@ public static async Task WriteImageToStream(Image x, string name, Stream imageSt // Add manifest JsonArray tagsNode = new() { - name + ":latest" // TODO: do something else here? + name + ":" + tag }; JsonNode manifestNode = new JsonArray(new JsonObject diff --git a/Microsoft.NET.Build.Containers/Registry.cs b/Microsoft.NET.Build.Containers/Registry.cs index c2d34937f792..904ec315fff4 100644 --- a/Microsoft.NET.Build.Containers/Registry.cs +++ b/Microsoft.NET.Build.Containers/Registry.cs @@ -169,8 +169,10 @@ private static HttpClient GetClient() return client; } - public async Task Push(Image x, string name, string baseName) + public async Task Push(Image x, string name, string? tag, string baseName) { + tag ??= "latest"; + using HttpClient client = GetClient(); foreach (var descriptor in x.LayerDescriptors) @@ -217,7 +219,7 @@ public async Task Push(Image x, string name, string baseName) putResponse.EnsureSuccessStatusCode(); - var putResponse2 = await client.PutAsync(new Uri(BaseUri, $"/v2/{name}/manifests/latest"), manifestUploadContent); + var putResponse2 = await client.PutAsync(new Uri(BaseUri, $"/v2/{name}/manifests/{tag}"), manifestUploadContent); putResponse2.EnsureSuccessStatusCode(); } diff --git a/containerize/Program.cs b/containerize/Program.cs index 2a449cd1ce6c..4a5f3644c995 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -84,19 +84,19 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry // File.WriteAllTextAsync("manifest.json", x.manifest.ToJsonString(options)); // File.WriteAllTextAsync("config.json", x.config.ToJsonString(options)); - await LocalDocker.Load(x, imageName, baseName); + await LocalDocker.Load(x, imageName, imageTag, baseName); - Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:latest' to run the application."); + Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:{imageTag}' to run the application."); } -static async Task PushToLocalDockerViaRegistry(string registryName, string baseName, string imageName, Registry registry, Image x) +static async Task PushToLocalDockerViaRegistry(string registryName, string baseName, string imageName, string imageTag, Registry registry, Image x) { // Push the image back to the local registry - await registry.Push(x, imageName, baseName); + await registry.Push(x, imageName, imageTag, baseName); - Console.WriteLine($"Pushed {registryName}/{imageName}:latest"); + Console.WriteLine($"Pushed {registryName}/{imageName}:{imageTag}"); - var pullBase = System.Diagnostics.Process.Start("docker", $"pull {registryName}/{imageName}:latest"); + var pullBase = System.Diagnostics.Process.Start("docker", $"pull {registryName}/{imageName}:{imageTag}"); await pullBase.WaitForExitAsync(); } \ No newline at end of file From 80aaccbbd94e81a3dc9fdb3c4a7f2a545c8e7e41 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Tue, 9 Aug 2022 13:09:17 -0500 Subject: [PATCH 0024/1880] Delete unused PushToLocalDockerViaRegistry (#77) warnings--; --- containerize/Program.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/containerize/Program.cs b/containerize/Program.cs index 4a5f3644c995..e6f4e193e84e 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -88,15 +88,3 @@ async Task Containerize(DirectoryInfo folder, string workingDir, string registry Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:{imageTag}' to run the application."); } - -static async Task PushToLocalDockerViaRegistry(string registryName, string baseName, string imageName, string imageTag, Registry registry, Image x) -{ - // Push the image back to the local registry - - await registry.Push(x, imageName, imageTag, baseName); - - Console.WriteLine($"Pushed {registryName}/{imageName}:{imageTag}"); - - var pullBase = System.Diagnostics.Process.Start("docker", $"pull {registryName}/{imageName}:{imageTag}"); - await pullBase.WaitForExitAsync(); -} \ No newline at end of file From a9ab3b658b8eed271669d2eaa054d488fbd38826 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Tue, 9 Aug 2022 12:13:31 -0500 Subject: [PATCH 0025/1880] Adopt some new async tar APIs --- Microsoft.NET.Build.Containers/LocalDocker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.NET.Build.Containers/LocalDocker.cs b/Microsoft.NET.Build.Containers/LocalDocker.cs index b6430bfd9362..a4d0c0fd58ad 100644 --- a/Microsoft.NET.Build.Containers/LocalDocker.cs +++ b/Microsoft.NET.Build.Containers/LocalDocker.cs @@ -50,7 +50,7 @@ public static async Task WriteImageToStream(Image x, string name, string tag, St // Stuff that (uncompressed) tarball into the image tar stream string layerTarballPath = $"{d.Digest.Substring("sha256:".Length)}/layer.tar"; - writer.WriteEntry(localPath, layerTarballPath); + await writer.WriteEntryAsync(localPath, layerTarballPath); layerTarballPaths.Add(layerTarballPath); } @@ -64,7 +64,7 @@ public static async Task WriteImageToStream(Image x, string name, string tag, St DataStream = configStream }; - writer.WriteEntry(configEntry); // TODO: asyncify these when API available (Preview 7) + await writer.WriteEntryAsync(configEntry); } // Add manifest @@ -87,7 +87,7 @@ public static async Task WriteImageToStream(Image x, string name, string tag, St DataStream = manifestStream }; - writer.WriteEntry(manifestEntry); // TODO: asyncify these when API available (Preview 7) + await writer.WriteEntryAsync(manifestEntry); } } } From ff8d4613ae0e095d60a069aab066a273ba596afc Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 9 Aug 2022 15:14:03 -0500 Subject: [PATCH 0026/1880] fix UsingTask declarations so that tasks can be consumed --- .../build/Microsoft.NET.Build.Containers.props | 4 ++-- docs/INTEGRATED-DEMO.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props index 86181319e0e9..942d2eae325d 100644 --- a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props +++ b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props @@ -9,6 +9,6 @@ - - + + \ No newline at end of file diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md index 53a2955d4aba..e4f263c11c14 100644 --- a/docs/INTEGRATED-DEMO.md +++ b/docs/INTEGRATED-DEMO.md @@ -20,7 +20,7 @@ dotnet nuget add source https://nuget.pkg.github.com/rainersigwald/index.json \ --store-password-in-clear-text --configfile nuget.config # add a reference to the package -dotnet add package Microsoft.NET.Build.Containers --version 0.1.3 +dotnet add package Microsoft.NET.Build.Containers --prerelease # publish your project dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer From d8fc5103d59f5af322de2d387cbeac2126171dfd Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 9 Aug 2022 16:10:12 -0500 Subject: [PATCH 0027/1880] Fix support for non-apphost scenarios by providing an array (#67) Accept arrays for endpoint and args, and bring the targets themselves under tests. --- .../CreateNewImage.cs | 10 +++++----- Microsoft.NET.Build.Containers/Image.cs | 8 +++++--- .../Microsoft.NET.Build.Containers.props | 2 +- .../Microsoft.NET.Build.Containers.targets | 20 ++++++++++++------- containerize/Program.cs | 6 +++--- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index 68d67807ce4c..42c625bf6e22 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -59,12 +59,12 @@ public class CreateNewImage : Microsoft.Build.Utilities.Task /// The entrypoint application of the container. /// [Required] - public string Entrypoint { get; set; } + public ITaskItem[] Entrypoint { get; set; } /// /// Arguments to pass alongside Entrypoint. /// - public string EntrypointArgs { get; set; } + public ITaskItem[] EntrypointArgs { get; set; } public CreateNewImage() { @@ -76,8 +76,8 @@ public CreateNewImage() ImageTag = ""; PublishDirectory = ""; WorkingDirectory = ""; - Entrypoint = ""; - EntrypointArgs = ""; + Entrypoint = Array.Empty(); + EntrypointArgs = Array.Empty(); } @@ -109,7 +109,7 @@ public override bool Execute() Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory); image.AddLayer(newLayer); - image.SetEntrypoint(Entrypoint, EntrypointArgs?.Split(' ').ToArray()); + image.SetEntrypoint(Entrypoint.Select(i => i.ItemSpec).ToArray(), EntrypointArgs.Select(i => i.ItemSpec).ToArray()); if (OutputRegistry.StartsWith("docker://")) { diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs index 8e3a77f956da..5bfc98625292 100644 --- a/Microsoft.NET.Build.Containers/Image.cs +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -58,7 +58,9 @@ private void RecalculateDigest() { manifest["config"]!["digest"] = GetDigest(config); } - public void SetEntrypoint(string executable, string[]? args = null) + static JsonArray ToJsonArray(string[] items) => new JsonArray(items.Where(s => !string.IsNullOrEmpty(s)).Select(s =>(JsonValue) s).ToArray()); + + public void SetEntrypoint(string[] executableArgs, string[]? args = null) { JsonObject? configObject = config["config"]!.AsObject(); @@ -67,7 +69,7 @@ public void SetEntrypoint(string executable, string[]? args = null) throw new NotImplementedException("Expected base image to have a config node"); } - configObject["Entrypoint"] = executable; + configObject["Entrypoint"] = ToJsonArray(executableArgs); if (args is null) { @@ -75,7 +77,7 @@ public void SetEntrypoint(string executable, string[]? args = null) } else { - configObject["Cmd"] = new JsonArray(args.Where(s => !string.IsNullOrEmpty(s)).Select(s =>(JsonObject)s).ToArray()); + configObject["Cmd"] = ToJsonArray(args); } RecalculateDigest(); diff --git a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props index 942d2eae325d..cd7b483fcd9c 100644 --- a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props +++ b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props @@ -5,7 +5,7 @@ $(MSBuildThisFileDirectory)..\$(taskForldername)\$(taskFramework) - $(CustomTasksFolder)\$(MSBuildThisFileName).dll + $(CustomTasksFolder)\$(MSBuildThisFileName).dll diff --git a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets index 75a7e3f97921..79e800903233 100644 --- a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets +++ b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets @@ -1,11 +1,16 @@ + + + <_IsAspNet Condition="@(ProjectCapability->Count()) > 0 and @(ProjectCapability->AnyHaveMetadataValue('Identity', 'AspNetCore'))">true + <_IsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true + + - <_IsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true <_ContainerBaseRegistry>https://mcr.microsoft.com <_ContainerBaseImageName Condition="'$(_IsSelfContained)' == 'true'">dotnet/runtime-deps - <_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == '' and @(ProjectCapability->AnyHaveMetadataValue('Identity', 'AspNetCore'))">dotnet/aspnet + <_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == '' and '$(_IsAspNet)' == 'true'">dotnet/aspnet <_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == ''">dotnet/runtime <_ContainerBaseImageTag>$(_TargetFrameworkVersionWithoutV) @@ -18,18 +23,19 @@ $(AssemblyName) $(Version) /app - dotnet $(TargetFileName) - $(ContainerWorkingDirectory)/$(AssemblyName)$(_NativeExecutableExtension) - - + + + + + - + $"{System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription[5]}.0"); -Option entrypoint = new( +Option entrypoint = new( name: "--entrypoint", - description: "Entrypoint application."); + description: "Entrypoint application command."); Option imageName = new( name: "--name", @@ -60,7 +60,7 @@ return await rootCommand.InvokeAsync(args); -async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string entrypoint, string imageName, string imageTag) +async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string imageName, string imageTag) { Registry registry = new Registry(new Uri($"http://{registryName}")); From 4b26a6f97740b60337b9762edc62e709e85fae5e Mon Sep 17 00:00:00 2001 From: Ben Villalobos <4691428+BenVillalobos@users.noreply.github.com> Date: Tue, 9 Aug 2022 14:39:47 -0700 Subject: [PATCH 0028/1880] Add IsValidRegistry, Ensure ports flow with registries (#55) * Add IsValidRegistry and tests * Ensure port tags along during registry parsing --- .../ContainerHelpers.cs | 30 ++++++++++++++++++- .../ParseContainerProperties.cs | 20 ++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/Microsoft.NET.Build.Containers/ContainerHelpers.cs b/Microsoft.NET.Build.Containers/ContainerHelpers.cs index a8f8cc624721..fc593200751a 100644 --- a/Microsoft.NET.Build.Containers/ContainerHelpers.cs +++ b/Microsoft.NET.Build.Containers/ContainerHelpers.cs @@ -30,6 +30,34 @@ public static class ContainerHelpers } } + /// + /// Ensures the given registry is valid. + /// + /// + /// + public static bool IsValidRegistry(string registryName) + { + // No scheme prefixed onto the registry + if (string.IsNullOrEmpty(registryName) || + (!registryName.StartsWith("http://") && + !registryName.StartsWith("https://") && + !registryName.StartsWith("docker://"))) + { + return false; + } + + try + { + UriBuilder uri = new UriBuilder(registryName); + } + catch + { + return false; + } + + return true; + } + /// /// Ensures the given image name is valid. /// Spec: https://github.com/opencontainers/distribution-spec/blob/4ab4752c3b86a926d7e5da84de64cbbdcc18d313/spec.md#pulling-manifests @@ -82,7 +110,7 @@ public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedCont // If the image has a ':', there's a tag we need to parse. int indexOfColon = image.IndexOf(':'); - containerRegistry = uri.Scheme + "://" + uri.Host; + containerRegistry = uri.Scheme + "://" + uri.Host + (uri.Port > 0 && !uri.IsDefaultPort ? ":" + uri.Port : ""); containerName = indexOfColon == -1 ? image : image.Substring(0, indexOfColon); containerTag = indexOfColon == -1 ? "" : image.Substring(indexOfColon + 1); return true; diff --git a/Microsoft.NET.Build.Containers/ParseContainerProperties.cs b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs index 1e3e26bd20ff..496773330e71 100644 --- a/Microsoft.NET.Build.Containers/ParseContainerProperties.cs +++ b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs @@ -74,6 +74,24 @@ public override bool Execute() return !Log.HasLoggedErrors; } + string registryToUse = string.Empty; + + if (!ContainerRegistry.StartsWith("http://") && + !ContainerRegistry.StartsWith("https://") && + !ContainerRegistry.StartsWith("docker://")) + { + // Default to https when no scheme is present: https://github.com/distribution/distribution/blob/26163d82560f4dda94bd7b87d587f94644c5af79/reference/normalize.go#L88 + registryToUse = "https://"; + } + + registryToUse += ContainerRegistry; + + if (!ContainerHelpers.IsValidRegistry(registryToUse)) + { + Log.LogError("Could not recognize registry '{0}'. Does your registry need a scheme, like 'https://'?", ContainerRegistry); + return !Log.HasLoggedErrors; + } + if (FullyQualifiedBaseImageName.Contains(' ') && BuildEngine != null) { Log.LogWarning($"{nameof(FullyQualifiedBaseImageName)} had spaces in it, replacing with dashes."); @@ -91,7 +109,7 @@ public override bool Execute() ParsedContainerRegistry = outputReg; ParsedContainerImage = outputImage; ParsedContainerTag = outputTag; - NewContainerRegistry = ContainerRegistry; + NewContainerRegistry = registryToUse; NewContainerImageName = ContainerImageName; NewContainerTag = ContainerImageTag; From 5a7a9486cb2e5c5f7dc124fe29f23900411696c1 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 9 Aug 2022 16:40:56 -0500 Subject: [PATCH 0029/1880] Add help documentation to repo (#80) * prevent Nerdbank from flowing through package dependencies * add package README and detailed help docs --- .../Microsoft.NET.Build.Containers.csproj | 2 + Microsoft.NET.Build.Containers/README.md | 20 +++++ docs/ContainerCustomization.md | 87 +++++++++++++++++++ docs/GettingStarted.md | 17 ++++ 4 files changed, 126 insertions(+) create mode 100644 Microsoft.NET.Build.Containers/README.md create mode 100644 docs/ContainerCustomization.md create mode 100644 docs/GettingStarted.md diff --git a/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj index a1472dcba1eb..4ac68496b3df 100644 --- a/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj +++ b/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj @@ -28,6 +28,7 @@ https://github.com/rainersigwald/containers git containers;docker;Microsoft.NET.Build.Containers + README.md @@ -37,6 +38,7 @@ + diff --git a/Microsoft.NET.Build.Containers/README.md b/Microsoft.NET.Build.Containers/README.md new file mode 100644 index 000000000000..004730b16a1c --- /dev/null +++ b/Microsoft.NET.Build.Containers/README.md @@ -0,0 +1,20 @@ +# .NET SDK Containers + +This package lets you build container images from your projects with a single command. + +## Getting Started + +To build a container from the SDK, add this package and run the `publish` command, +specifying the `DefaultContainer` PublishProfile. You can learn more about Publish Profiles [in the documentation](https://docs.microsoft.com/aspnet/core/host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-6.0#publish-profiles). + +```shell +>dotnet add package Microsoft.NET.Build.Containers --prerelease +>dotnet publish --os linux --arch x64 -p:ProfileName=DefaultContainer +... +Pushed container ':' to registry 'docker://' +... +``` + +Out of the box, this package will infer a number of properties about the generated container image, including which base image to use, which version of that image to use, and where to push the generated image. You have control over all of these properties, however. You can read more about these customizations [here](https://aka.ms/dotnet/containers/customization). + +**Note**: This package only supports Linux containers in this version. diff --git a/docs/ContainerCustomization.md b/docs/ContainerCustomization.md new file mode 100644 index 000000000000..41e5ee896481 --- /dev/null +++ b/docs/ContainerCustomization.md @@ -0,0 +1,87 @@ +# Customizing your container + +You can control many aspects of the generated container through MSBuild properties. In general, if you could use a command in a Dockerfile to set some configuration, you can do the same via MSBuild. + +> **Note** +> The only exception to this is `RUN` commands - due to the way we build containers, those cannot be emulated. If you need this functionality, you will need to use a Dockerfile to build your container images. + +> **Note** +> This package only supports Linux containers in this version. + +## ContainerBaseImage + +This property controls the image used as the basis for your image. By default, we will infer the following values for you based on the properties of your project: + +* if your project is self-contained, we use the `mcr.microsoft.com/dotnet/runtime-deps` image as the base image +* if your project is an ASP.NET Core project, we use the `mcr.microsoft.com/dotnet/aspnet` image as the base image +* otherwise we use the `mcr.microsoft.com/dotnet/runtime` image as the base image + +We infer the tag of the image to be the numeric component of your chosen `TargetFramework` - so a `.net6.0` project will use the `6.0` tag of the inferred base image, a `.net7.0-linux` project will use the `7.0` tag, and so on. + +If you set a value here, you should set the fully-qualified name of the image to use as the base, including any tag you prefer: + +```xml +mcr.microsoft.com/dotnet/runtime:6.0 +``` + +## ContainerRegistry + +This property controls the destination registry - the place that the newly-created image will be pushed to. + +Be default, we push to the local Docker daemon (annotated by `docker://`), but for this release you can specify any _unauthenticated_ registry. For example: + +```xml +registry.mycorp.com:1234 +``` + +> **Note** +> There is no authentication currently supported - that [will come in a future release](https://github.com/rainersigwald/containers/issues/70) - so make sure you're pointing to an unauthenticated registry + +## ContainerImageName + +This property controls the name of the image itself, e.g `dotnet/runtime` or `my-awesome-app`. + +By default, the value used will be the `AssemblyName` of the project. + + +```xml +my-super-awesome-app +``` + +> **Note** +> Image names can only contain lowercase alphanumeric characters, periods, underscores, and dashes, and must start with a letter or number - any other characters will result in an error being thrown. + +## ContainerImageTag + +This property controls the tag that is generated for the image. Tags are often used to refer to different versions of an application, but they can also refer to different operating system distributions, or even just different baked-in configuration. + +By default, the value used will be the `Version` of the project. + +```xml +1.2.3-alpha2 +``` + +> **Note** +> Tags can only contain up to 127 alphanumeric characters, periods, underscores, and dashes. They must start with an alphanumeric character or an underscore. Any other form will result in an error being thrown. + +## ContainerWorkingDirectory + +This property controls the working directory of the container - the directory that commands are executed within if not other command is run. + +By default, we use the `/app` directory as the working directory. + +```xml +/bin +``` + +## Unsupported properties + +There are many other properties and items that we want to add support for in subsequent previews: + +* Entrypoints +* Entrypoint Arguments +* Ports +* Environment Variables +* Labels + +We expect to add them in future versions, so watch this space! diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 000000000000..5f63ff91f385 --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,17 @@ +# Getting Started + +To build a container from the SDK, add this package and run the `publish` command, +specifying the `DefaultContainer` PublishProfile. You can learn more about Publish Profiles [in the documentation](https://docs.microsoft.com/aspnet/core/host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-6.0#publish-profiles). + +```shell +>dotnet add package Microsoft.NET.Build.Containers --prerelease +>dotnet publish --os linux --arch x64 -p:ProfileName=DefaultContainer +... +Pushed container ':' to registry 'docker://' +... +``` + +Out of the box, this package will infer a number of properties about the generated container image, including which base image to use, which version of that image to use, and where to push the generated image. You have control over all of these properties, however. You can read more about customizing the container [here](./ContainerCustomization.md) + +> **Note** +> This package only supports Linux containers in this version. From 94417ea9cd5d83634ce32c29f34f826378e0bc54 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 9 Aug 2022 17:03:24 -0500 Subject: [PATCH 0030/1880] add note about WebSDK support (#90) --- Microsoft.NET.Build.Containers/README.md | 2 ++ docs/GettingStarted.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Microsoft.NET.Build.Containers/README.md b/Microsoft.NET.Build.Containers/README.md index 004730b16a1c..8912bc7d9011 100644 --- a/Microsoft.NET.Build.Containers/README.md +++ b/Microsoft.NET.Build.Containers/README.md @@ -18,3 +18,5 @@ Pushed container ':' to registry 'docker://' Out of the box, this package will infer a number of properties about the generated container image, including which base image to use, which version of that image to use, and where to push the generated image. You have control over all of these properties, however. You can read more about these customizations [here](https://aka.ms/dotnet/containers/customization). **Note**: This package only supports Linux containers in this version. + +**Note**: This package only supports Web projects (those that use the `Microsoft.NET.Sdk.Web` SDK) in this version. \ No newline at end of file diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 5f63ff91f385..b448833a1d6b 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -15,3 +15,6 @@ Out of the box, this package will infer a number of properties about the generat > **Note** > This package only supports Linux containers in this version. + +> **Note** +> This package only supports Web projects (those that use the `Microsoft.NET.Sdk.Web` SDK) in this version. \ No newline at end of file From eb171f21adf7f3e42f2e0a51271611bf808b05dc Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Tue, 9 Aug 2022 17:19:32 -0500 Subject: [PATCH 0031/1880] Update timestamp when modifying config (#89) Fixes #62. --- Microsoft.NET.Build.Containers/Image.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs index 5bfc98625292..372c0ebc08f4 100644 --- a/Microsoft.NET.Build.Containers/Image.cs +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -54,7 +54,10 @@ public void AddLayer(Layer l) RecalculateDigest(); } - private void RecalculateDigest() { + private void RecalculateDigest() + { + config["created"] = DateTime.UtcNow; + manifest["config"]!["digest"] = GetDigest(config); } From 7d4f14008103821c5959fe9fd7f4089d9bc37e09 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Wed, 10 Aug 2022 10:09:34 -0500 Subject: [PATCH 0032/1880] Entrypoint args to CreateNewImage are now arrays (#101) Fixes #98. --- .../build/Microsoft.NET.Build.Containers.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets index 79e800903233..d27e5687b7c0 100644 --- a/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets +++ b/Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets @@ -71,7 +71,7 @@ ImageTag="$(ContainerImageTag)" PublishDirectory="$(PublishDir)" WorkingDirectory="$(ContainerWorkingDirectory)" - Entrypoint="$(ContainerEntrypoint)" - EntrypointArgs="$(ContainerEntrypointArgs)"/> + Entrypoint="@(ContainerEntrypoint)" + EntrypointArgs="@(ContainerEntrypointArgs)"/> \ No newline at end of file From 50f81d2a09374aae077e2a023f8e52873d54e556 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 10 Aug 2022 13:12:06 -0500 Subject: [PATCH 0033/1880] Update INTEGRATED-DEMO.md with more details (#103) * Update INTEGRATED-DEMO.md * Update INTEGRATED-DEMO.md --- docs/INTEGRATED-DEMO.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md index e4f263c11c14..7fb3a8c88ccd 100644 --- a/docs/INTEGRATED-DEMO.md +++ b/docs/INTEGRATED-DEMO.md @@ -3,6 +3,15 @@ This guidance will track the most up-to-date version of the package and tasks. You should expect it to shrink noticeably over time! +## Prerequisites + +* Docker should be running +* You should have an environment variable called GITHUB_USERNAME, with your github username in it +* You should have an environment variable called GITHUB_TOKEN, with a github [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) that has `read:packages` permissions. + + +## Usage + ```bash # create a new project and move to its directory dotnet new web -n my-awesome-container-app @@ -20,12 +29,14 @@ dotnet nuget add source https://nuget.pkg.github.com/rainersigwald/index.json \ --store-password-in-clear-text --configfile nuget.config # add a reference to the package -dotnet add package Microsoft.NET.Build.Containers --prerelease +dotnet add package Microsoft.NET.Build.Containers # publish your project dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer # run your app -docker run -it --rm my-awesome-container-app:latest +docker run -it --rm -p 5010:80 my-awesome-container-app:latest ``` +Now you can go to `localhost:5010` and you should see the `Hello World!` text! + From 21ec518bca7081e82a11867f03f8c4dfaf97654d Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 10 Aug 2022 14:23:18 -0500 Subject: [PATCH 0034/1880] Fix version on sample code (#104) --- docs/INTEGRATED-DEMO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md index 7fb3a8c88ccd..2de02ec9b625 100644 --- a/docs/INTEGRATED-DEMO.md +++ b/docs/INTEGRATED-DEMO.md @@ -35,7 +35,7 @@ dotnet add package Microsoft.NET.Build.Containers dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer # run your app -docker run -it --rm -p 5010:80 my-awesome-container-app:latest +docker run -it --rm -p 5010:80 my-awesome-container-app:1.0.0 ``` Now you can go to `localhost:5010` and you should see the `Hello World!` text! From 2e069fa706dfa021cbd8cd339c12751feab7b5b0 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Wed, 10 Aug 2022 17:22:23 -0500 Subject: [PATCH 0035/1880] Detect and report docker load errors (#107) We may want to have a more sophisticated way of dealing with this in the future but this gives users a fighting chance of understand what went wrong + better error reports for us. --- .../CreateNewImage.cs | 10 +++++++- .../DockerLoadException.cs | 23 +++++++++++++++++++ Microsoft.NET.Build.Containers/LocalDocker.cs | 6 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 Microsoft.NET.Build.Containers/DockerLoadException.cs diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index 42c625bf6e22..01da8ce7b5be 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -113,7 +113,15 @@ public override bool Execute() if (OutputRegistry.StartsWith("docker://")) { - LocalDocker.Load(image, ImageName, ImageTag, BaseImageName).Wait(); + try + { + LocalDocker.Load(image, ImageName, ImageTag, BaseImageName).Wait(); + } + catch (AggregateException ex) when (ex.InnerException is DockerLoadException dle) + { + Log.LogErrorFromException(dle, showStackTrace: false); + return !Log.HasLoggedErrors; + } } else { diff --git a/Microsoft.NET.Build.Containers/DockerLoadException.cs b/Microsoft.NET.Build.Containers/DockerLoadException.cs new file mode 100644 index 000000000000..fb08518af83e --- /dev/null +++ b/Microsoft.NET.Build.Containers/DockerLoadException.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Microsoft.NET.Build.Containers +{ + public class DockerLoadException : Exception + { + public DockerLoadException() + { + } + + public DockerLoadException(string? message) : base(message) + { + } + + public DockerLoadException(string? message, Exception? innerException) : base(message, innerException) + { + } + + protected DockerLoadException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/Microsoft.NET.Build.Containers/LocalDocker.cs b/Microsoft.NET.Build.Containers/LocalDocker.cs index a4d0c0fd58ad..d2a4d47bc820 100644 --- a/Microsoft.NET.Build.Containers/LocalDocker.cs +++ b/Microsoft.NET.Build.Containers/LocalDocker.cs @@ -14,6 +14,7 @@ public static async Task Load(Image x, string name, string tag, string baseName) ProcessStartInfo loadInfo = new("docker", $"load"); loadInfo.RedirectStandardInput = true; loadInfo.RedirectStandardOutput = true; + loadInfo.RedirectStandardError = true; using Process? loadProcess = Process.Start(loadInfo); @@ -29,6 +30,11 @@ public static async Task Load(Image x, string name, string tag, string baseName) loadProcess.StandardInput.Close(); await loadProcess.WaitForExitAsync(); + + if (loadProcess.ExitCode != 0) + { + throw new DockerLoadException($"Failed to load image to local Docker daemon. stdout: {await loadProcess.StandardError.ReadToEndAsync()}"); + } } public static async Task WriteImageToStream(Image x, string name, string tag, Stream imageStream) From 38a01d275d292b3bc1485cc6bb6ae1c13c5ebe0f Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Wed, 10 Aug 2022 17:39:39 -0500 Subject: [PATCH 0036/1880] First dogfooder doc feedback (#105) * Expand prereqs to include preview 7 * Use DOS-style quoting If you don't do this, in cmd shell, the single quotes can make their way into the nuget.config. * Explicitly mention Linux containers Otherwise, `docker load` in publish will fail. * Link to Microsoft docs on linux containers --- docs/INTEGRATED-DEMO.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/INTEGRATED-DEMO.md b/docs/INTEGRATED-DEMO.md index 2de02ec9b625..5736de3a692d 100644 --- a/docs/INTEGRATED-DEMO.md +++ b/docs/INTEGRATED-DEMO.md @@ -5,7 +5,9 @@ You should expect it to shrink noticeably over time! ## Prerequisites -* Docker should be running +* [.NET SDK 7.0.100-preview.7](https://dotnet.microsoft.com/download/dotnet/7.0) or higher +* Docker should be installed and running +* On Windows, Docker must be [configured for Linux containers](https://docs.microsoft.com/virtualization/windowscontainers/quick-start/quick-start-windows-10-linux) * You should have an environment variable called GITHUB_USERNAME, with your github username in it * You should have an environment variable called GITHUB_TOKEN, with a github [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) that has `read:packages` permissions. @@ -25,7 +27,7 @@ dotnet new nugetconfig # GITHUB_TOKEN environment variables being present, the token should have 'read:packages' # permissions. (replace the \ with ` if using powershell) dotnet nuget add source https://nuget.pkg.github.com/rainersigwald/index.json \ - --name rainer --username '%GITHUB_USERNAME%' --password '%GITHUB_TOKEN%' \ + --name rainer --username "%GITHUB_USERNAME%" --password "%GITHUB_TOKEN%" \ --store-password-in-clear-text --configfile nuget.config # add a reference to the package From 6e7773f7b6bb8bc3ec6812be6291038d67a1dd94 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 11 Aug 2022 10:20:44 -0500 Subject: [PATCH 0037/1880] Autocorrect image names if they are correctable (#108) * correct input container image names if they are correctable * another test * propose warning code * remove unused pattern --- .../ContainerHelpers.cs | 38 ++++++++++++++++--- .../ParseContainerProperties.cs | 25 +++++++++--- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/Microsoft.NET.Build.Containers/ContainerHelpers.cs b/Microsoft.NET.Build.Containers/ContainerHelpers.cs index fc593200751a..046e02fae39e 100644 --- a/Microsoft.NET.Build.Containers/ContainerHelpers.cs +++ b/Microsoft.NET.Build.Containers/ContainerHelpers.cs @@ -9,6 +9,11 @@ public static class ContainerHelpers private static Regex imageNameRegex = new Regex("^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*$"); + /// + /// Matches if the string is not lowercase or numeric, or ., _, or -. + /// + private static Regex imageNameCharacters = new Regex("[^a-zA-Z0-9._-]"); + /// /// Given some "fully qualified" image name (e.g. mcr.microsoft.com/dotnet/runtime), return /// a valid UriBuilder. This means appending 'https' if the URI is not absolute, otherwise UriBuilder will throw. @@ -39,8 +44,8 @@ public static bool IsValidRegistry(string registryName) { // No scheme prefixed onto the registry if (string.IsNullOrEmpty(registryName) || - (!registryName.StartsWith("http://") && - !registryName.StartsWith("https://") && + (!registryName.StartsWith("http://") && + !registryName.StartsWith("https://") && !registryName.StartsWith("docker://"))) { return false; @@ -89,9 +94,9 @@ public static bool IsValidImageTag(string imageTag) /// /// /// True if the parse was successful. When false is returned, all out vars are set to empty strings. - public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedContainerName, - [NotNullWhen(true)] out string? containerRegistry, - [NotNullWhen(true)] out string? containerName, + public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedContainerName, + [NotNullWhen(true)] out string? containerRegistry, + [NotNullWhen(true)] out string? containerName, [NotNullWhen(true)] out string? containerTag) { Uri? uri = ContainerImageToUri(fullyQualifiedContainerName); @@ -115,4 +120,27 @@ public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedCont containerTag = indexOfColon == -1 ? "" : image.Substring(indexOfColon + 1); return true; } + + /// + /// Checks if a given container image name adheres to the image name spec. If not, and recoverable, then normalizes invalid characters. + /// + public static bool NormalizeImageName(string containerImageName, [NotNullWhen(false)] out string? normalizedImageName) + { + if (IsValidImageName(containerImageName)) + { + normalizedImageName = null; + return true; + } + else + { + if (Char.IsUpper(containerImageName, 0)) + { + containerImageName = Char.ToLowerInvariant(containerImageName[0]) + containerImageName[1..]; + } else if (!Char.IsLetterOrDigit(containerImageName, 0)) { + throw new ArgumentException("The first character of the image name must be a lowercase letter or a digit."); + } + normalizedImageName = imageNameCharacters.Replace(containerImageName, "-"); + return false; + } + } } diff --git a/Microsoft.NET.Build.Containers/ParseContainerProperties.cs b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs index 496773330e71..47487e542a68 100644 --- a/Microsoft.NET.Build.Containers/ParseContainerProperties.cs +++ b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs @@ -62,11 +62,6 @@ public ParseContainerProperties() public override bool Execute() { - if (!ContainerHelpers.IsValidImageName(ContainerImageName)) - { - Log.LogError($"Invalid {nameof(ContainerImageName)}: {0}", ContainerImageName); - return !Log.HasLoggedErrors; - } if (!string.IsNullOrEmpty(ContainerImageTag) && !ContainerHelpers.IsValidImageTag(ContainerImageTag)) { @@ -106,11 +101,29 @@ public override bool Execute() return !Log.HasLoggedErrors; } + try + { + if (!ContainerHelpers.NormalizeImageName(ContainerImageName, out string? normalizedImageName)) + { + Log.LogWarning(null, "CONTAINER001", null, null, 0, 0, 0, 0, $"{nameof(ContainerImageName)} was not a valid container image name, it was normalized to {normalizedImageName}"); + NewContainerImageName = normalizedImageName; + } + else + { + // name was valid already + NewContainerImageName = ContainerImageName; + } + } + catch (ArgumentException) + { + Log.LogError($"Invalid {nameof(ContainerImageName)}: {{0}}", ContainerImageName); + return !Log.HasLoggedErrors; + } + ParsedContainerRegistry = outputReg; ParsedContainerImage = outputImage; ParsedContainerTag = outputTag; NewContainerRegistry = registryToUse; - NewContainerImageName = ContainerImageName; NewContainerTag = ContainerImageTag; if (BuildEngine != null) From 6dd6892880b861da48995e50ea15f3f01dc54eb7 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 11 Aug 2022 10:20:55 -0500 Subject: [PATCH 0038/1880] Recursively walk input files to include in the container (#110) recursively walk input files --- Microsoft.NET.Build.Containers/Layer.cs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/Microsoft.NET.Build.Containers/Layer.cs b/Microsoft.NET.Build.Containers/Layer.cs index 62cfe08c5d04..3d35cb681f9c 100644 --- a/Microsoft.NET.Build.Containers/Layer.cs +++ b/Microsoft.NET.Build.Containers/Layer.cs @@ -11,20 +11,14 @@ public record struct Layer public static Layer FromDirectory(string directory, string containerPath) { - DirectoryInfo di = new(directory); - - IEnumerable<(string path, string containerPath)> fileList = - di.GetFileSystemInfos() - .Where(fsi => fsi is FileInfo).Select( - fsi => - { - string destinationPath = - Path.Join(containerPath, - Path.GetRelativePath(directory, fsi.FullName)) - .Replace(Path.DirectorySeparatorChar, '/'); - return (fsi.FullName, destinationPath); - }); - + var fileList = + new DirectoryInfo(directory) + .EnumerateFiles("*", SearchOption.AllDirectories) + .Select(fsi => + { + string destinationPath = Path.Join(containerPath, Path.GetRelativePath(directory, fsi.FullName)).Replace(Path.DirectorySeparatorChar, '/'); + return (fsi.FullName, destinationPath); + }); return FromFiles(fileList); } From 85ed55dcc00681176284e798c42761feb0966b64 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Thu, 11 Aug 2022 13:11:12 -0500 Subject: [PATCH 0039/1880] Set image working directory (#111) set the working directory of the generated container --- Microsoft.NET.Build.Containers/CreateNewImage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index 01da8ce7b5be..718238b80804 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -109,6 +109,7 @@ public override bool Execute() Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory); image.AddLayer(newLayer); + image.WorkingDirectory = WorkingDirectory; image.SetEntrypoint(Entrypoint.Select(i => i.ItemSpec).ToArray(), EntrypointArgs.Select(i => i.ItemSpec).ToArray()); if (OutputRegistry.StartsWith("docker://")) From 7060b4984ff20cdbf9e469d01547a604b940bf92 Mon Sep 17 00:00:00 2001 From: Tim Heuer Date: Fri, 19 Aug 2022 08:07:37 -0700 Subject: [PATCH 0040/1880] Add support for Labels to the Image config (#120) * Add support for Labels in metadata, fixes #95 Co-authored-by: Chet Husk --- .../CreateNewImage.cs | 11 ++++ Microsoft.NET.Build.Containers/Image.cs | 55 +++++++++++++++++-- .../Microsoft.NET.Build.Containers.targets | 14 +++-- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index 718238b80804..a343fda5f4da 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -66,6 +66,11 @@ public class CreateNewImage : Microsoft.Build.Utilities.Task /// public ITaskItem[] EntrypointArgs { get; set; } + /// + /// Labels that the image configuration will include in metadata + /// + public ITaskItem[] Labels { get; set; } + public CreateNewImage() { BaseRegistry = ""; @@ -78,6 +83,7 @@ public CreateNewImage() WorkingDirectory = ""; Entrypoint = Array.Empty(); EntrypointArgs = Array.Empty(); + Labels = Array.Empty(); } @@ -112,6 +118,11 @@ public override bool Execute() image.WorkingDirectory = WorkingDirectory; image.SetEntrypoint(Entrypoint.Select(i => i.ItemSpec).ToArray(), EntrypointArgs.Select(i => i.ItemSpec).ToArray()); + foreach (var label in Labels) + { + image.Label(label.ItemSpec, label.GetMetadata("Value")); + } + if (OutputRegistry.StartsWith("docker://")) { try diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs index 372c0ebc08f4..68379de36f3b 100644 --- a/Microsoft.NET.Build.Containers/Image.cs +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -5,6 +5,8 @@ namespace Microsoft.NET.Build.Containers; +record Label(string name, string value); + public class Image { public JsonNode manifest; @@ -15,12 +17,16 @@ public class Image internal List newLayers = new(); + private HashSet