diff --git a/src/Microsoft.Tye.Hosting.Diagnostics/DiagnosticOptions.cs b/src/Microsoft.Tye.Hosting.Diagnostics/DiagnosticOptions.cs index 249e9360..5e77bc75 100644 --- a/src/Microsoft.Tye.Hosting.Diagnostics/DiagnosticOptions.cs +++ b/src/Microsoft.Tye.Hosting.Diagnostics/DiagnosticOptions.cs @@ -13,27 +13,14 @@ namespace Microsoft.Tye.Hosting.Diagnostics public (string Key, string Value) DistributedTraceProvider { get; set; } public (string Key, string Value) MetricsProvider { get; set; } - public static DiagnosticOptions FromConfiguration(IConfiguration configuration) + public static (string, string) GetProvider(string text) { - return new DiagnosticOptions - { - LoggingProvider = GetProvider(configuration, "logs"), - DistributedTraceProvider = GetProvider(configuration, "dtrace"), - MetricsProvider = GetProvider(configuration, "metrics") - }; - } - - private static (string, string) GetProvider(IConfiguration configuration, string providerName) - { - var providerString = configuration[providerName]; - - if (string.IsNullOrEmpty(providerString)) + if (string.IsNullOrEmpty(text)) { return (null, null); } - var pair = providerString.Split('='); - + var pair = text.Split('='); if (pair.Length < 2) { return (pair[0].Trim(), null); diff --git a/src/Microsoft.Tye.Hosting/HostOptions.cs b/src/Microsoft.Tye.Hosting/HostOptions.cs new file mode 100644 index 00000000..d7439c26 --- /dev/null +++ b/src/Microsoft.Tye.Hosting/HostOptions.cs @@ -0,0 +1,24 @@ +// 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.Collections.Generic; +using Microsoft.Tye.Hosting.Diagnostics; + +namespace Microsoft.Tye.Hosting +{ + public class HostOptions + { + public bool Dashboard { get; set; } + + public List Debug { get; } = new List(); + + public DiagnosticOptions Diagnostics { get; } = new DiagnosticOptions(); + + public bool Docker { get; set; } + + public bool NoBuild { get; set; } + + public int? Port { get; set; } + } +} diff --git a/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs b/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs index 6c95321c..c311753f 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs @@ -14,14 +14,14 @@ namespace Microsoft.Tye.Hosting public string[]? ServicesToDebug { get; set; } public bool DebugAllServices { get; set; } - public static ProcessRunnerOptions FromArgs(string[] args, string[] servicesToDebug) + public static ProcessRunnerOptions FromHostOptions(HostOptions options) { return new ProcessRunnerOptions { - BuildProjects = !args.Contains("--no-build"), - DebugMode = args.Contains("--debug"), - ServicesToDebug = servicesToDebug, - DebugAllServices = servicesToDebug?.Contains("*", StringComparer.OrdinalIgnoreCase) ?? false + BuildProjects = !options.NoBuild, + DebugMode = options.Debug.Any(), + ServicesToDebug = options.Debug.ToArray(), + DebugAllServices = options.Debug?.Contains("*", StringComparer.OrdinalIgnoreCase) ?? false }; } } diff --git a/src/Microsoft.Tye.Hosting/TyeHost.cs b/src/Microsoft.Tye.Hosting/TyeHost.cs index b40d95d3..67925db2 100644 --- a/src/Microsoft.Tye.Hosting/TyeHost.cs +++ b/src/Microsoft.Tye.Hosting/TyeHost.cs @@ -36,21 +36,13 @@ namespace Microsoft.Tye.Hosting private AggregateApplicationProcessor? _processor; private readonly Application _application; - private readonly string[] _args; - private readonly string[] _servicesToDebug; - + private readonly HostOptions _options; private ReplicaRegistry? _replicaRegistry; - public TyeHost(Application application, string[] args) - : this(application, args, new string[0]) - { - } - - public TyeHost(Application application, string[] args, string[] servicesToDebug) + public TyeHost(Application application, HostOptions options) { _application = application; - _args = args; - _servicesToDebug = servicesToDebug; + _options = options; } public Application Application => _application; @@ -78,7 +70,7 @@ namespace Microsoft.Tye.Hosting public async Task StartAsync() { - var app = BuildWebApplication(_application, _args, Sink); + var app = BuildWebApplication(_application, _options, Sink); DashboardWebApplication = app; _logger = app.Logger; @@ -88,11 +80,9 @@ namespace Microsoft.Tye.Hosting ConfigureApplication(app); - var configuration = app.Configuration; - _replicaRegistry = new ReplicaRegistry(_application.ContextDirectory, _logger); - _processor = CreateApplicationProcessor(_replicaRegistry, _args, _servicesToDebug, _logger, configuration); + _processor = CreateApplicationProcessor(_replicaRegistry, _options, _logger); await app.StartAsync(); @@ -100,7 +90,7 @@ namespace Microsoft.Tye.Hosting await _processor.StartAsync(_application); - if (_args.Contains("--dashboard")) + if (_options.Dashboard) { OpenDashboard(app.Addresses.First()); } @@ -108,12 +98,16 @@ namespace Microsoft.Tye.Hosting return app; } - private static WebApplication BuildWebApplication( - Application application, - string[] args, - ILogEventSink? sink) + private static WebApplication BuildWebApplication(Application application, HostOptions options, ILogEventSink? sink) { - var builder = WebApplication.CreateBuilder(args); + var args = new List(); + if (options.Port.HasValue) + { + args.Add("--port"); + args.Add(options.Port.Value.ToString(CultureInfo.InvariantCulture)); + } + + var builder = WebApplication.CreateBuilder(args.ToArray()); // Logging for this application builder.Host.UseSerilog((context, configuration) => @@ -252,13 +246,12 @@ namespace Microsoft.Tye.Hosting return false; } - private static AggregateApplicationProcessor CreateApplicationProcessor(ReplicaRegistry replicaRegistry, string[] args, string[] servicesToDebug, Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration) + private static AggregateApplicationProcessor CreateApplicationProcessor(ReplicaRegistry replicaRegistry, HostOptions options, Microsoft.Extensions.Logging.ILogger logger) { - var diagnosticOptions = DiagnosticOptions.FromConfiguration(configuration); - var diagnosticsCollector = new DiagnosticsCollector(logger, diagnosticOptions); + var diagnosticsCollector = new DiagnosticsCollector(logger, options.Diagnostics); // Print out what providers were selected and their values - diagnosticOptions.DumpDiagnostics(logger); + options.Diagnostics.DumpDiagnostics(logger); var processors = new List { @@ -268,11 +261,11 @@ namespace Microsoft.Tye.Hosting new HttpProxyService(logger), new DockerImagePuller(logger), new DockerRunner(logger, replicaRegistry), - new ProcessRunner(logger, replicaRegistry, ProcessRunnerOptions.FromArgs(args, servicesToDebug)) + new ProcessRunner(logger, replicaRegistry, ProcessRunnerOptions.FromHostOptions(options)) }; // If the docker command is specified then transform the ProjectRunInfo into DockerRunInfo - if (args.Contains("--docker")) + if (options.Docker) { processors.Insert(0, new TransformProjectsIntoContainers(logger)); } diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index c3abef4f..d9bfe1e7 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -11,77 +11,82 @@ using System.Threading; using Microsoft.Tye.ConfigModel; using Microsoft.Tye.Extensions; using Microsoft.Tye.Hosting; +using Microsoft.Tye.Hosting.Diagnostics; namespace Microsoft.Tye { static partial class Program { - private static Command CreateRunCommand(string[] args) + private static Command CreateRunCommand() { var command = new Command("run", "run the application") { CommonArguments.Path_Required, - }; - - // TODO: We'll need to support a --build-args - command.AddOption(new Option("--no-build") - { - Description = "Do not build project files before running.", - Required = false - }); - - command.AddOption(new Option("--port") - { - Description = "The port to run control plane on.", - Argument = new Argument("port"), - Required = false - }); - - command.AddOption(new Option("--logs") - { - Description = "Write structured application logs to the specified log providers. Supported providers are console, elastic (Elasticsearch), ai (ApplicationInsights), seq.", - Argument = new Argument("logs"), - Required = false - }); - - command.AddOption(new Option("--dtrace") - { - Description = "Write distributed traces to the specified providers. Supported providers are zipkin.", - Argument = new Argument("logs"), - Required = false - }); - - command.AddOption(new Option("--debug") - { - Argument = new Argument("service"), - Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.", - Required = false - }); - command.AddOption(new Option("--docker") - { - Description = "Run projects as docker containers.", - Required = false - }); + new Option("--no-build") + { + Description = "Do not build project files before running.", + Required = false + }, + new Option("--port") + { + Description = "The port to run control plane on.", + Argument = new Argument("port"), + Required = false + }, + new Option("--logs") + { + Description = "Write structured application logs to the specified log provider. Supported providers are 'console', 'elastic' (Elasticsearch), 'ai' (ApplicationInsights), 'seq'.", + Argument = new Argument("logs"), + Required = false + }, + new Option("--dtrace") + { + Description = "Write distributed traces to the specified tracing provider. Supported providers are 'zipkin'.", + Argument = new Argument("trace"), + Required = false, + }, + new Option("--metrics") + { + Description = "Write metrics to the specified metrics provider.", + Argument = new Argument("metrics"), + Required = false + }, + new Option("--debug") + { + Argument = new Argument("service") + { + Arity = ArgumentArity.ZeroOrMore, + }, + Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.", + Required = false + }, + new Option("--docker") + { + Description = "Run projects as docker containers.", + Required = false + }, + new Option("--dashboard") + { + Description = "Launch dashboard on run.", + Required = false + }, - command.AddOption(new Option("--dashboard") - { - Description = "Launch dashboard on run.", - Required = false - }); + StandardOptions.Verbosity, + }; - command.Handler = CommandHandler.Create(async (console, path, debug) => + command.Handler = CommandHandler.Create(async args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 - if (path is null) + if (args.Path is null) { throw new CommandException("No project or solution file was found."); } - var output = new OutputContext(console, Verbosity.Info); + var output = new OutputContext(args.Console, Verbosity.Info); output.WriteInfoLine("Loading Application Details..."); - var application = await ApplicationFactory.CreateAsync(output, path); + var application = await ApplicationFactory.CreateAsync(output, args.Path); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); @@ -93,7 +98,24 @@ namespace Microsoft.Tye output.WriteInfoLine("Launching Tye Host..."); output.WriteInfoLine(string.Empty); - await using var host = new TyeHost(application.ToHostingApplication(), args, debug); + + var options = new HostOptions() + { + Dashboard = args.Dashboard, + Docker = args.Docker, + NoBuild = args.NoBuild, + Port = args.Port, + + Diagnostics = + { + DistributedTraceProvider = DiagnosticOptions.GetProvider(args.Dtrace), + LoggingProvider = DiagnosticOptions.GetProvider(args.Logs), + MetricsProvider = DiagnosticOptions.GetProvider(args.Metrics), + }, + }; + options.Debug.AddRange(args.Debug); + + await using var host = new TyeHost(application.ToHostingApplication(), options); await host.RunAsync(); }); @@ -114,5 +136,30 @@ namespace Microsoft.Tye // We use serviceCount * 4 because we currently launch multiple processes per service, this gives the dashboard some breathing room } + + // We have too many options to use the lambda form with each option as a parameter. + // This is slightly cleaner anyway. + private class RunCommandArguments + { + public IConsole Console { get; set; } = default!; + + public bool Dashboard { get; set; } + + public string[] Debug { get; set; } = Array.Empty(); + + public string Dtrace { get; set; } = default!; + + public bool Docker { get; set; } + + public string Logs { get; set; } = default!; + + public string Metrics { get; set; } = default!; + + public bool NoBuild { get; set; } + + public FileInfo Path { get; set; } = default!; + + public int? Port { get; set; } + } } } diff --git a/src/tye/Program.cs b/src/tye/Program.cs index 0f0a7814..eabd8abb 100644 --- a/src/tye/Program.cs +++ b/src/tye/Program.cs @@ -26,7 +26,7 @@ namespace Microsoft.Tye command.AddCommand(CreateInitCommand()); command.AddCommand(CreateGenerateCommand()); - command.AddCommand(CreateRunCommand(args)); + command.AddCommand(CreateRunCommand()); command.AddCommand(CreateBuildCommand()); command.AddCommand(CreatePushCommand()); command.AddCommand(CreateDeployCommand()); diff --git a/test/E2ETest/TyePurgeTests.cs b/test/E2ETest/TyePurgeTests.cs index 399efdde..240d089c 100644 --- a/test/E2ETest/TyePurgeTests.cs +++ b/test/E2ETest/TyePurgeTests.cs @@ -38,7 +38,7 @@ namespace E2ETest var tyeDir = new DirectoryInfo(Path.Combine(projectDirectory.DirectoryPath, ".tye")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); - var host = new TyeHost(application.ToHostingApplication(), Array.Empty()) + var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; @@ -76,7 +76,7 @@ namespace E2ETest var tyeDir = new DirectoryInfo(Path.Combine(projectDirectory.DirectoryPath, ".tye")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); - var host = new TyeHost(application.ToHostingApplication(), Array.Empty()) + var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index b96ed29f..ab5bc227 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -61,7 +61,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, Array.Empty(), async (app, uri) => + await RunHostingApplication(application, new HostOptions(), async (app, uri) => { var frontendUri = await GetServiceUrl(client, uri, "frontend"); var backendUri = await GetServiceUrl(client, uri, "backend"); @@ -92,7 +92,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, new[] { "--docker" }, async (app, uri) => + await RunHostingApplication(application, new HostOptions() { Docker = true, }, async (app, uri) => { // Make sure we're running containers Assert.True(app.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo)); @@ -128,7 +128,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, new[] { "--docker" }, async (app, uri) => + await RunHostingApplication(application, new HostOptions() { Docker = true, }, async (app, uri) => { // Make sure we're running containers Assert.True(app.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo)); @@ -181,7 +181,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, Array.Empty(), async (app, uri) => + await RunHostingApplication(application, new HostOptions(), async (app, uri) => { var frontendUri = await GetServiceUrl(client, uri, "frontend"); var backendUri = await GetServiceUrl(client, uri, "backend"); @@ -226,7 +226,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, Array.Empty(), async (app, uri) => + await RunHostingApplication(application, new HostOptions(), async (app, uri) => { var frontendUri = await GetServiceUrl(client, uri, "frontend"); var backendUri = await GetServiceUrl(client, uri, "backend"); @@ -263,9 +263,8 @@ namespace E2ETest }; var client = new HttpClient(new RetryHandler(handler)); - var args = new[] { "--docker" }; - await RunHostingApplication(application, args, async (app, serviceApi) => + await RunHostingApplication(application, new HostOptions() { Docker = true, }, async (app, serviceApi) => { var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test"); @@ -280,7 +279,7 @@ namespace E2ETest Assert.Equal("Things saved to the volume!", await client.GetStringAsync(serviceUri)); }); - await RunHostingApplication(application, args, async (app, serviceApi) => + await RunHostingApplication(application, new HostOptions() { Docker = true, }, async (app, serviceApi) => { var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test"); @@ -322,7 +321,7 @@ namespace E2ETest { await RunHostingApplication( application, - new[] { "--docker" }, + new HostOptions() { Docker = true, }, async (app, uri) => { // Make sure we're running containers @@ -374,7 +373,7 @@ namespace E2ETest await RunHostingApplication( application, - new[] { "--docker" }, + new HostOptions() { Docker = true, }, async (app, uri) => { // Make sure we're running containers @@ -423,9 +422,12 @@ namespace E2ETest await File.WriteAllTextAsync(Path.Combine(tempDir.DirectoryPath, "file.txt"), "This content came from the host"); var client = new HttpClient(new RetryHandler(handler)); - var args = new[] { "--docker" }; + var options = new HostOptions() + { + Docker = true, + }; - await RunHostingApplication(application, args, async (app, serviceApi) => + await RunHostingApplication(application, options, async (app, serviceApi) => { var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test"); @@ -453,7 +455,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, Array.Empty(), async (app, uri) => + await RunHostingApplication(application, new HostOptions(), async (app, uri) => { var ingressUri = await GetServiceUrl(client, uri, "ingress"); var appAUri = await GetServiceUrl(client, uri, "app-a"); @@ -502,7 +504,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, Array.Empty(), async (app, uri) => + await RunHostingApplication(application, new HostOptions(), async (app, uri) => { var nginxUri = await GetServiceUrl(client, uri, "nginx"); var appAUri = await GetServiceUrl(client, uri, "appA"); @@ -534,7 +536,7 @@ namespace E2ETest // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); - await using var host = new TyeHost(application.ToHostingApplication(), Array.Empty()) + await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; @@ -562,7 +564,7 @@ namespace E2ETest var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, Array.Empty(), async (app, uri) => + await RunHostingApplication(application, new HostOptions(), async (app, uri) => { var votingUri = await GetServiceUrl(client, uri, "vote"); var workerUri = await GetServiceUrl(client, uri, "worker"); @@ -607,7 +609,7 @@ services: var client = new HttpClient(new RetryHandler(handler)); - await RunHostingApplication(application, Array.Empty(), async (app, uri) => + await RunHostingApplication(application, new HostOptions(), async (app, uri) => { var votingUri = await GetServiceUrl(client, uri, "vote"); var workerUri = await GetServiceUrl(client, uri, "worker"); @@ -628,9 +630,9 @@ services: return $"{binding.Protocol ?? "http"}://localhost:{binding.Port}"; } - private async Task RunHostingApplication(ApplicationBuilder application, string[] args, Func execute) + private async Task RunHostingApplication(ApplicationBuilder application, HostOptions options, Func execute) { - await using var host = new TyeHost(application.ToHostingApplication(), args) + await using var host = new TyeHost(application.ToHostingApplication(), options) { Sink = _sink, };