diff --git a/src/Tye.Core/Tye.Core.csproj b/src/Tye.Core/Tye.Core.csproj index ddb2ac90..e9362bcc 100644 --- a/src/Tye.Core/Tye.Core.csproj +++ b/src/Tye.Core/Tye.Core.csproj @@ -1,6 +1,6 @@ - + - + netstandard2.1 Tye 8.0 @@ -33,7 +33,7 @@ - + diff --git a/src/tye/ConfigModel/ConfigFactory.cs b/src/tye/ConfigModel/ConfigFactory.cs index 0f59317a..9dca2876 100644 --- a/src/tye/ConfigModel/ConfigFactory.cs +++ b/src/tye/ConfigModel/ConfigFactory.cs @@ -65,8 +65,7 @@ namespace Tye.ConfigModel continue; } - var projectFilePath = project.AbsolutePath.Replace('\\', Path.DirectorySeparatorChar); - var extension = Path.GetExtension(projectFilePath).ToLower(); + var extension = Path.GetExtension(project.AbsolutePath).ToLower(); switch (extension) { case ".csproj": @@ -76,7 +75,7 @@ namespace Tye.ConfigModel continue; } - var description = CreateService(new FileInfo(projectFilePath)); + var description = CreateService(new FileInfo(project.AbsolutePath.Replace('\\', '/'))); if (description != null) { application.Services.Add(description); @@ -140,7 +139,7 @@ namespace Tye.ConfigModel var service = new ConfigService() { Name = Path.GetFileNameWithoutExtension(file.Name).ToLowerInvariant(), - Project = file.FullName, + Project = file.FullName.Replace('\\', '/'), }; PopulateFromLaunchProfile(service, launchProfile); diff --git a/src/tye/InitHost.cs b/src/tye/InitHost.cs new file mode 100644 index 00000000..96a43e9b --- /dev/null +++ b/src/tye/InitHost.cs @@ -0,0 +1,112 @@ +using System; +using System.CommandLine; +using System.CommandLine.IO; +using System.IO; +using Tye.ConfigModel; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Tye +{ + public class InitHost + { + + public static string CreateTyeFile(FileInfo? path, bool force) + { + var (content, outputFilePath) = CreateTyeFileContent(path, force); + + File.WriteAllText(outputFilePath, content); + + return outputFilePath; + } + + public static (string, string) CreateTyeFileContent(FileInfo? path, bool force) + { + if (path is FileInfo && path.Exists && !force) + { + ThrowIfTyeFilePresent(path, "tye.yml"); + ThrowIfTyeFilePresent(path, "tye.yaml"); + } + + var template = @" +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +# +# define global settings here +# name: exampleapp # application name +# registry: exampleuser # dockerhub username or container registry hostname + +# define multiple services here +services: +- name: myservice + # project: app.csproj # msbuild project path (relative to this file) + # executable: app.exe # path to an executable (relative to this file) + # args: --arg1=3 # arguments to pass to the process + # replicas: 5 # number of times to launch the application + # env: # array of environment variables + # - name: key + # value: value + # bindings: # optional array of bindings (ports, connection strings) + # - port: 8080 # number port of the binding +".TrimStart(); + + // Output in the current directory unless an input file was provided, then + // output next to the input file. + var outputFilePath = "tye.yaml"; + + if (path is FileInfo && path.Exists) + { + var application = ConfigFactory.FromFile(path); + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) + .Build(); + + var extension = path.Extension.ToLowerInvariant(); + var directory = path.Directory; + + // Clear all bindings if any for solutions and project files + if (extension == ".sln" || extension == ".csproj" || extension == ".fsproj") + { + // If the input file is a project or solution then use that as the name + application.Name = Path.GetFileNameWithoutExtension(path.Name).ToLowerInvariant(); + + foreach (var service in application.Services) + { + service.Bindings = null!; + service.Configuration = null!; + service.Project = service.Project!.Substring(directory.FullName.Length).TrimStart('/'); + } + + // If the input file is a sln/project then place the config next to it + outputFilePath = Path.Combine(directory.FullName, "tye.yaml"); + } + else + { + // If the input file is a yaml, then use the directory name. + application.Name = path.Directory.Name.ToLowerInvariant(); + + // If the input file is a yaml, then replace it. + outputFilePath = path.FullName; + } + + template = @" +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +".TrimStart() + serializer.Serialize(application); + } + + return (template, outputFilePath); + } + + + private static void ThrowIfTyeFilePresent(FileInfo? path, string yml) + { + var tyeYaml = Path.Combine(path!.DirectoryName, yml); + if (File.Exists(tyeYaml)) + { + throw new CommandException($"File '{tyeYaml}' already exists. Use --force to override the {yml} file if desired."); + } + } + } +} diff --git a/src/tye/Program.InitCommand.cs b/src/tye/Program.InitCommand.cs index 98aff009..dadb739a 100644 --- a/src/tye/Program.InitCommand.cs +++ b/src/tye/Program.InitCommand.cs @@ -29,95 +29,11 @@ namespace Tye command.Handler = CommandHandler.Create((console, path, force) => { - var output = new OutputContext(console, Verbosity.Info); - if (path is FileInfo && path.Exists && !force) - { - ThrowIfTyeFilePresent(path, "tye.yml"); - ThrowIfTyeFilePresent(path, "tye.yaml"); - } - - var template = @" -# tye application configuration file -# read all about it at https://github.com/dotnet/tye -# -# define global settings here -# name: exampleapp # application name -# registry: exampleuser # dockerhub username or container registry hostname - -# define multiple services here -services: -- name: myservice - # project: app.csproj # msbuild project path (relative to this file) - # executable: app.exe # path to an executable (relative to this file) - # args: --arg1=3 # arguments to pass to the process - # replicas: 5 # number of times to launch the application - # env: # array of environment variables - # - name: key - # value: value - # bindings: # optional array of bindings (ports, connection strings) - # - port: 8080 # number port of the binding -".TrimStart(); - - // Output in the current directory unless an input file was provided, then - // output next to the input file. - var outputFilePath = "tye.yaml"; - - if (path is FileInfo && path.Exists) - { - var application = ConfigFactory.FromFile(path); - var serializer = new SerializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) - .Build(); - - var extension = path.Extension.ToLowerInvariant(); - var directory = path.Directory; - - // Clear all bindings if any for solutions and project files - if (extension == ".sln" || extension == ".csproj" || extension == ".fsproj") - { - // If the input file is a project or solution then use that as the name - application.Name = Path.GetFileNameWithoutExtension(path.Name).ToLowerInvariant(); - - foreach (var service in application.Services) - { - service.Bindings = null!; - service.Configuration = null!; - service.Project = service.Project!.Substring(directory.FullName.Length).TrimStart(Path.DirectorySeparatorChar); - } - - // If the input file is a sln/project then place the config next to it - outputFilePath = Path.Combine(directory.FullName, "tye.yaml"); - } - else - { - // If the input file is a yaml, then use the directory name. - application.Name = path.Directory.Name.ToLowerInvariant(); - - // If the input file is a yaml, then replace it. - outputFilePath = path.FullName; - } - - template = @" -# tye application configuration file -# read all about it at https://github.com/dotnet/tye -".TrimStart() + serializer.Serialize(application); - } - - File.WriteAllText(outputFilePath, template); + var outputFilePath = InitHost.CreateTyeFile(path, force); console.Out.WriteLine($"Created '{outputFilePath}'."); }); return command; } - - private static void ThrowIfTyeFilePresent(FileInfo? path, string yml) - { - var tyeYaml = Path.Combine(path!.DirectoryName, yml); - if (File.Exists(tyeYaml)) - { - throw new CommandException($"File '{tyeYaml}' already exists. Use --force to override the {yml} file if desired."); - } - } } } diff --git a/test/E2ETest/E2ETest.csproj b/test/E2ETest/E2ETest.csproj index e3b3106e..2a9ad66f 100644 --- a/test/E2ETest/E2ETest.csproj +++ b/test/E2ETest/E2ETest.csproj @@ -18,4 +18,13 @@ + + + + + + + + + diff --git a/test/E2ETest/TestHelpers.cs b/test/E2ETest/TestHelpers.cs new file mode 100644 index 00000000..96690e0d --- /dev/null +++ b/test/E2ETest/TestHelpers.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace E2ETest +{ + public static class TestHelpers + { + // https://github.com/dotnet/aspnetcore/blob/5a0526dfd991419d5bce0d8ea525b50df2e37b04/src/Testing/src/TestPathUtilities.cs + // This can get into a bad pattern for having crazy paths in places. Eventually, especially if we use helix, + // we may want to avoid relying on sln position. + public static string GetSolutionRootDirectory(string solution) + { + var applicationBasePath = AppContext.BaseDirectory; + var directoryInfo = new DirectoryInfo(applicationBasePath); + + do + { + var projectFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, $"{solution}.sln")); + if (projectFileInfo.Exists) + { + return projectFileInfo.DirectoryName; + } + + directoryInfo = directoryInfo.Parent; + } + while (directoryInfo.Parent != null); + + throw new Exception($"Solution file {solution}.sln could not be found in {applicationBasePath} or its parent directories."); + } + } +} diff --git a/test/E2ETest/TyeInitTests.cs b/test/E2ETest/TyeInitTests.cs new file mode 100644 index 00000000..cc837594 --- /dev/null +++ b/test/E2ETest/TyeInitTests.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Text.Json; +using Microsoft.Build.Construction; +using Tye; +using Tye.ConfigModel; +using Xunit; +using Xunit.Abstractions; + +namespace E2ETest +{ + public class TyeInitTests + { + private readonly ITestOutputHelper output; + private readonly TestOutputLogEventSink sink; + + public TyeInitTests(ITestOutputHelper output) + { + this.output = output; + sink = new TestOutputLogEventSink(output); + } + + [Fact] + public void SingleProjectInitTest() + { + var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "single-project", "test-project")); + using var tempDirectory = TempDirectory.Create(); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj")); + + var (content, _) = InitHost.CreateTyeFileContent(projectFile, force: false); + var expectedContent = File.ReadAllText("testassets/init/single-project.yaml"); + + output.WriteLine(content); + + Assert.Equal(expectedContent, content); + } + + [Fact] + public void MultiProjectInitTest() + { + var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "multi-project")); + using var tempDirectory = TempDirectory.Create(); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + // delete already present yaml + File.Delete(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "multi-project.sln")); + + var (content, _) = InitHost.CreateTyeFileContent(projectFile, force: false); + var expectedContent = File.ReadAllText("testassets/init/multi-project.yaml"); + + output.WriteLine(content); + + Assert.Equal(expectedContent, content); + } + + [Fact] + public void FrontendBackendTest() + { + var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend")); + using var tempDirectory = TempDirectory.Create(); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + // delete already present yaml + File.Delete(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "frontend-backend.sln")); + + var (content, _) = InitHost.CreateTyeFileContent(projectFile, force: false); + var expectedContent = File.ReadAllText("testassets/init/frontend-backend.yaml"); + + output.WriteLine(content); + + Assert.Equal(expectedContent, content); + } + } +} diff --git a/test/E2ETest/TyeRunTest.cs b/test/E2ETest/TyeRunTests.cs similarity index 75% rename from test/E2ETest/TyeRunTest.cs rename to test/E2ETest/TyeRunTests.cs index 21be7316..08c078ff 100644 --- a/test/E2ETest/TyeRunTest.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -16,22 +16,21 @@ using Xunit.Abstractions; namespace E2ETest { - public class TyeRunTest + public class TyeRunTests { private readonly ITestOutputHelper output; private readonly TestOutputLogEventSink sink; - public TyeRunTest(ITestOutputHelper output) + public TyeRunTests(ITestOutputHelper output) { this.output = output; sink = new TestOutputLogEventSink(output); } - [Fact] public async Task SingleProjectTest() { - var projectDirectory = new DirectoryInfo(Path.Combine(GetSolutionRootDirectory("tye"), "samples", "single-project", "test-project")); + var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "single-project", "test-project")); using var tempDirectory = TempDirectory.Create(); DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); @@ -97,29 +96,5 @@ namespace E2ETest await host.StopAsync(); } } - - - // https://github.com/dotnet/aspnetcore/blob/5a0526dfd991419d5bce0d8ea525b50df2e37b04/src/Testing/src/TestPathUtilities.cs - // This can get into a bad pattern for having crazy paths in places. Eventually, especially if we use helix, - // we may want to avoid relying on sln position. - public static string GetSolutionRootDirectory(string solution) - { - var applicationBasePath = AppContext.BaseDirectory; - var directoryInfo = new DirectoryInfo(applicationBasePath); - - do - { - var projectFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, $"{solution}.sln")); - if (projectFileInfo.Exists) - { - return projectFileInfo.DirectoryName; - } - - directoryInfo = directoryInfo.Parent; - } - while (directoryInfo.Parent != null); - - throw new Exception($"Solution file {solution}.sln could not be found in {applicationBasePath} or its parent directories."); - } } } diff --git a/test/E2ETest/testassets/init/frontend-backend.yaml b/test/E2ETest/testassets/init/frontend-backend.yaml new file mode 100644 index 00000000..26fafede --- /dev/null +++ b/test/E2ETest/testassets/init/frontend-backend.yaml @@ -0,0 +1,8 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: frontend-backend +services: +- name: backend + project: backend/backend.csproj +- name: frontend + project: frontend/frontend.csproj diff --git a/test/E2ETest/testassets/init/multi-project.yaml b/test/E2ETest/testassets/init/multi-project.yaml new file mode 100644 index 00000000..8a5c2f4d --- /dev/null +++ b/test/E2ETest/testassets/init/multi-project.yaml @@ -0,0 +1,10 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: multi-project +services: +- name: backend + project: backend/backend.csproj +- name: frontend + project: frontend/frontend.csproj +- name: worker + project: worker/worker.csproj diff --git a/test/E2ETest/testassets/init/single-project.yaml b/test/E2ETest/testassets/init/single-project.yaml new file mode 100644 index 00000000..9a428e03 --- /dev/null +++ b/test/E2ETest/testassets/init/single-project.yaml @@ -0,0 +1,6 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: test-project +services: +- name: test-project + project: test-project.csproj