From d0773bcd52f7ae050477714660ffce38b5850573 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 1 Mar 2020 13:34:25 -0800 Subject: [PATCH] Improve factoring of configuration - Separate 'Hosting.Model' from the YAML processing - Introduce 'Config*' classes as YAML DTOs - Refactor 'Hosting.Model' classes to be nullable-friendly - Fix nullable warnings in M8s.Hosting - Add support for registry from config - Add application name to config - Fix a bug with the wrong file-location used for tye.yaml - Fix a bug with `tye deploy` at solution scope --- .../DiagnosticsCollector.cs | 25 +-- .../Dashboard/Pages/Index.razor | 8 +- .../Dashboard/Pages/Logs.razor | 6 +- .../Dashboard/Pages/ServiceDetails.razor | 6 +- .../Dashboard/Shared/NavMenu.razor | 2 +- .../Micronetes.Hosting/DockerRunner.cs | 37 ++-- .../EventPipeDiagnosticsRunner.cs | 19 +- .../Infrastructure/ProcessUtil.cs | 12 +- .../Micronetes.Hosting.csproj | 2 +- .../Micronetes.Hosting/MicronetesHost.cs | 2 +- .../Micronetes.Hosting/Model/Application.cs | 195 +----------------- .../Micronetes.Hosting/Model/DockerRunInfo.cs | 15 ++ .../Model/ExecutableRunInfo.cs | 18 ++ .../Model/ProjectRunInfo.cs | 16 ++ .../Micronetes.Hosting/Model/RunInfo.cs | 6 + .../Micronetes.Hosting/Model/Service.cs | 39 ++-- .../Model/ServiceBinding.cs | 8 +- .../Model/ServiceDescription.cs | 38 ++-- .../Micronetes.Hosting/ProcessRunner.cs | 70 ++++--- .../Micronetes.Hosting/ProxyService.cs | 18 +- src/tye/ConfigModel/ConfigApplication.cs | 77 +++++++ .../ConfigModel/ConfigConfigurationSource.cs | 13 ++ src/tye/ConfigModel/ConfigFactory.cs | 176 ++++++++++++++++ src/tye/ConfigModel/ConfigService.cs | 24 +++ src/tye/ConfigModel/ConfigServiceBinding.cs | 15 ++ src/tye/OpulenceApplicationAdapter.cs | 7 +- src/tye/Program.DeployCommand.cs | 37 ++-- src/tye/Program.InitCommand.cs | 82 ++++---- src/tye/Program.RunCommand.cs | 5 +- 29 files changed, 595 insertions(+), 383 deletions(-) create mode 100644 src/micronetes/Micronetes.Hosting/Model/DockerRunInfo.cs create mode 100644 src/micronetes/Micronetes.Hosting/Model/ExecutableRunInfo.cs create mode 100644 src/micronetes/Micronetes.Hosting/Model/ProjectRunInfo.cs create mode 100644 src/micronetes/Micronetes.Hosting/Model/RunInfo.cs create mode 100644 src/tye/ConfigModel/ConfigApplication.cs create mode 100644 src/tye/ConfigModel/ConfigConfigurationSource.cs create mode 100644 src/tye/ConfigModel/ConfigFactory.cs create mode 100644 src/tye/ConfigModel/ConfigService.cs create mode 100644 src/tye/ConfigModel/ConfigServiceBinding.cs diff --git a/src/micronetes/Micronetes.Hosting.Diagnostics/DiagnosticsCollector.cs b/src/micronetes/Micronetes.Hosting.Diagnostics/DiagnosticsCollector.cs index 09c1d613..be335d34 100644 --- a/src/micronetes/Micronetes.Hosting.Diagnostics/DiagnosticsCollector.cs +++ b/src/micronetes/Micronetes.Hosting.Diagnostics/DiagnosticsCollector.cs @@ -83,16 +83,17 @@ namespace Micronetes.Hosting.Diagnostics _options = options; } - public void ProcessEvents(string applicationName, - string serviceName, - int processId, - string replicaName, - IDictionary metrics, - CancellationToken cancellationToken) + public void ProcessEvents( + string applicationName, + string serviceName, + int processId, + string replicaName, + IDictionary metrics, + CancellationToken cancellationToken) { var hasEventPipe = false; - for (int i = 0; i < 10; ++i) + for (var i = 0; i < 10; ++i) { if (DiagnosticsClient.GetPublishedProcesses().Contains(processId)) { @@ -267,7 +268,7 @@ namespace Micronetes.Hosting.Diagnostics private void HandleLoggingEvents(EventPipeEventSource source, ILoggerFactory loggerFactory, string replicaName) { - string lastFormattedMessage = ""; + var lastFormattedMessage = ""; var logActivities = new Dictionary(); var stack = new Stack(); @@ -357,8 +358,8 @@ namespace Micronetes.Hosting.Diagnostics { var formatString = formatElement.GetString(); var formatter = new LogValuesFormatter(formatString); - object[] args = new object[formatter.ValueNames.Count]; - for (int i = 0; i < args.Length; i++) + var args = new object[formatter.ValueNames.Count]; + for (var i = 0; i < args.Length; i++) { args[i] = message.GetProperty(formatter.ValueNames[i]).GetString(); } @@ -413,7 +414,7 @@ namespace Micronetes.Hosting.Diagnostics var payloadVal = (IDictionary)traceEvent.PayloadValue(0); var eventPayload = (IDictionary)payloadVal["Payload"]; - ICounterPayload payload = CounterPayload.FromPayload(eventPayload); + var payload = CounterPayload.FromPayload(eventPayload); metrics[traceEvent.ProviderName + "/" + payload.Name] = payload.Value; } @@ -497,7 +498,7 @@ namespace Micronetes.Hosting.Diagnostics { var (activityId, duration) = GetActivityStop(arguments); - int statusCode = 0; + var statusCode = 0; foreach (var arg in arguments) { diff --git a/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Index.razor b/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Index.razor index 1d2d6a0c..aeb64073 100644 --- a/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Index.razor +++ b/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Index.razor @@ -26,13 +26,13 @@ @service.ServiceType - @if (service.Description.Project != null) + @if (service.Description.RunInfo is ProjectRunInfo project) { -

@service.Description.Project

+

@project.Project

} - else if (service.Description.DockerImage != null) + else if (service.Description.RunInfo is DockerRunInfo docker) { -

@service.Description.DockerImage

+

@docker.Image

} diff --git a/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Logs.razor b/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Logs.razor index 8a849804..8014cb3f 100644 --- a/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Logs.razor +++ b/src/micronetes/Micronetes.Hosting/Dashboard/Pages/Logs.razor @@ -20,11 +20,11 @@ else @code { [Parameter] - public string ServiceName { get; set; } + public string ServiceName { get; set; } = default!; - public List<(string Text, int Id)> ApplicationLogs { get; set; } + public List<(string Text, int Id)>? ApplicationLogs { get; set; } - private IDisposable _subscription; + private IDisposable? _subscription; protected override void OnInitialized() { diff --git a/src/micronetes/Micronetes.Hosting/Dashboard/Pages/ServiceDetails.razor b/src/micronetes/Micronetes.Hosting/Dashboard/Pages/ServiceDetails.razor index 0f3d1d26..f82e7edf 100644 --- a/src/micronetes/Micronetes.Hosting/Dashboard/Pages/ServiceDetails.razor +++ b/src/micronetes/Micronetes.Hosting/Dashboard/Pages/ServiceDetails.razor @@ -21,12 +21,12 @@ else } @code { - private Service _service; + private Service? _service; [Parameter] - public string ServiceName { get; set; } + public string ServiceName { get; set; } = default!; - public Service Service => _service; + public Service? Service => _service; protected override void OnInitialized() { diff --git a/src/micronetes/Micronetes.Hosting/Dashboard/Shared/NavMenu.razor b/src/micronetes/Micronetes.Hosting/Dashboard/Shared/NavMenu.razor index bf27d966..7bd6652d 100644 --- a/src/micronetes/Micronetes.Hosting/Dashboard/Shared/NavMenu.razor +++ b/src/micronetes/Micronetes.Hosting/Dashboard/Shared/NavMenu.razor @@ -21,7 +21,7 @@ @code { private bool collapseNavMenu = true; - private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; + private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { diff --git a/src/micronetes/Micronetes.Hosting/DockerRunner.cs b/src/micronetes/Micronetes.Hosting/DockerRunner.cs index 963d58c3..1848cf1d 100644 --- a/src/micronetes/Micronetes.Hosting/DockerRunner.cs +++ b/src/micronetes/Micronetes.Hosting/DockerRunner.cs @@ -24,7 +24,7 @@ namespace Micronetes.Hosting var index = 0; foreach (var s in application.Services) { - tasks[index++] = s.Value.Description.External ? Task.CompletedTask : StartContainerAsync(application, s.Value); + tasks[index++] = s.Value.Description.RunInfo is DockerRunInfo docker ? StartContainerAsync(application, s.Value, docker) : Task.CompletedTask; } return Task.WhenAll(tasks); @@ -45,13 +45,8 @@ namespace Micronetes.Hosting return Task.WhenAll(tasks); } - private async Task StartContainerAsync(Application application, Service service) + private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker) { - if (service.Description.DockerImage == null) - { - return; - } - if (!await _dockerInstalled.Value) { _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not installed.", service.Description.Name); @@ -63,12 +58,9 @@ namespace Micronetes.Hosting var serviceDescription = service.Description; var environmentArguments = ""; - var dockerInfo = new DockerInformation() - { - Tasks = new Task[service.Description.Replicas.Value] - }; + var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]); - async Task RunDockerContainer(IEnumerable<(int Port, int? InternalPort, int BindingPort, string Protocol)> ports) + async Task RunDockerContainer(IEnumerable<(int Port, int? InternalPort, int BindingPort, string? Protocol)> ports) { var hasPorts = ports.Any(); @@ -109,7 +101,7 @@ namespace Micronetes.Hosting environmentArguments += $"-e {pair.Key}={pair.Value} "; } - var command = $"run -d {environmentArguments} {portString} --name {replica} --restart=unless-stopped {service.Description.DockerImage} {service.Description.Args ?? ""}"; + var command = $"run -d {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}"; _logger.LogInformation("Running docker command {Command}", command); service.Logs.OnNext($"[{replica}]: {command}"); @@ -133,7 +125,7 @@ namespace Micronetes.Hosting return; } - var containerId = result.StandardOutput.Trim(); + var containerId = (string?)result.StandardOutput.Trim(); // There's a race condition that sometimes makes us miss the output // so keep trying to get the container id @@ -194,9 +186,9 @@ namespace Micronetes.Hosting { // Each replica is assigned a list of internal ports, one mapped to each external // port - for (int i = 0; i < serviceDescription.Replicas; i++) + for (var i = 0; i < serviceDescription.Replicas; i++) { - var ports = new List<(int, int?, int, string)>(); + var ports = new List<(int, int?, int, string?)>(); foreach (var binding in serviceDescription.Bindings) { if (binding.Port == null) @@ -212,9 +204,9 @@ namespace Micronetes.Hosting } else { - for (int i = 0; i < service.Description.Replicas; i++) + for (var i = 0; i < service.Description.Replicas; i++) { - dockerInfo.Tasks[i] = RunDockerContainer(Enumerable.Empty<(int, int?, int, string)>()); + dockerInfo.Tasks[i] = RunDockerContainer(Enumerable.Empty<(int, int?, int, string?)>()); } } @@ -265,8 +257,13 @@ namespace Micronetes.Hosting private class DockerInformation { - public Task[] Tasks { get; set; } - public CancellationTokenSource StoppingTokenSource { get; set; } = new CancellationTokenSource(); + public DockerInformation(Task[] tasks) + { + Tasks = tasks; + } + + public Task[] Tasks { get; } + public CancellationTokenSource StoppingTokenSource { get; } = new CancellationTokenSource(); } } } diff --git a/src/micronetes/Micronetes.Hosting/EventPipeDiagnosticsRunner.cs b/src/micronetes/Micronetes.Hosting/EventPipeDiagnosticsRunner.cs index dcf84882..d5fb1c31 100644 --- a/src/micronetes/Micronetes.Hosting/EventPipeDiagnosticsRunner.cs +++ b/src/micronetes/Micronetes.Hosting/EventPipeDiagnosticsRunner.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Micronetes.Hosting.Diagnostics; @@ -25,7 +23,7 @@ namespace Micronetes.Hosting { foreach (var service in application.Services.Values) { - if (service.Description.External) + if (service.Description.RunInfo is null) { continue; } @@ -70,12 +68,13 @@ namespace Micronetes.Hosting Thread = new Thread(() => { // TODO: Finding the application name requires msbuild knowledge - _diagnosticsCollector.ProcessEvents(Path.GetFileNameWithoutExtension(process.Service.Status.ProjectFilePath), - process.Service.Description.Name, - process.Pid.Value, - replica.Name, - replica.Metrics, - cts.Token); + _diagnosticsCollector.ProcessEvents( + Path.GetFileNameWithoutExtension(process.Service.Status.ProjectFilePath), + process.Service.Description.Name, + process.Pid!.Value, + replica.Name, + replica.Metrics, + cts.Token); }) }; @@ -104,7 +103,7 @@ namespace Micronetes.Hosting private class Subscription { } private class DiagnosticsState { - public Thread Thread { get; set; } + public Thread Thread { get; set; } = default!; public CancellationTokenSource StoppingTokenSource { get; set; } = new CancellationTokenSource(); } } diff --git a/src/micronetes/Micronetes.Hosting/Infrastructure/ProcessUtil.cs b/src/micronetes/Micronetes.Hosting/Infrastructure/ProcessUtil.cs index c983e705..3c81feeb 100644 --- a/src/micronetes/Micronetes.Hosting/Infrastructure/ProcessUtil.cs +++ b/src/micronetes/Micronetes.Hosting/Infrastructure/ProcessUtil.cs @@ -14,12 +14,12 @@ namespace Micronetes.Hosting public static async Task RunAsync( string filename, string arguments, - string workingDirectory = null, + string? workingDirectory = null, bool throwOnError = true, - IDictionary environmentVariables = null, - Action outputDataReceived = null, - Action errorDataReceived = null, - Action onStart = null, + IDictionary? environmentVariables = null, + Action? outputDataReceived = null, + Action? errorDataReceived = null, + Action? onStart = null, CancellationToken cancellationToken = default) { using var process = new Process() @@ -106,7 +106,7 @@ namespace Micronetes.Hosting process.BeginOutputReadLine(); process.BeginErrorReadLine(); - var cancelledTcs = new TaskCompletionSource(); + var cancelledTcs = new TaskCompletionSource(); using var _ = cancellationToken.Register(() => cancelledTcs.TrySetResult(null)); var result = await Task.WhenAny(processLifetimeTask.Task, cancelledTcs.Task); diff --git a/src/micronetes/Micronetes.Hosting/Micronetes.Hosting.csproj b/src/micronetes/Micronetes.Hosting/Micronetes.Hosting.csproj index 93125759..08482f09 100644 --- a/src/micronetes/Micronetes.Hosting/Micronetes.Hosting.csproj +++ b/src/micronetes/Micronetes.Hosting/Micronetes.Hosting.csproj @@ -6,7 +6,7 @@ Orchestration host APIs. true true - disable + enable diff --git a/src/micronetes/Micronetes.Hosting/MicronetesHost.cs b/src/micronetes/Micronetes.Hosting/MicronetesHost.cs index 64698dd7..18264ead 100644 --- a/src/micronetes/Micronetes.Hosting/MicronetesHost.cs +++ b/src/micronetes/Micronetes.Hosting/MicronetesHost.cs @@ -95,7 +95,7 @@ namespace Micronetes.Hosting logger.LogError(0, ex, "Failed to launch application"); } - var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); lifetime.ApplicationStopping.Register(obj => waitForStop.TrySetResult(null), null); await waitForStop.Task; diff --git a/src/micronetes/Micronetes.Hosting/Model/Application.cs b/src/micronetes/Micronetes.Hosting/Model/Application.cs index 1f45195a..fe091570 100644 --- a/src/micronetes/Micronetes.Hosting/Model/Application.cs +++ b/src/micronetes/Micronetes.Hosting/Model/Application.cs @@ -1,184 +1,25 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text.Json; -using Microsoft.Build.Construction; -using Tye; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace Micronetes.Hosting.Model { public class Application { - public string ContextDirectory { get; set; } = Directory.GetCurrentDirectory(); - - public string Source { get; set; } - - public Application(IEnumerable services) + public Application(FileInfo source, Dictionary services) { - var map = new Dictionary(); - - // TODO: Do validation here - foreach (var s in services) - { - s.Replicas ??= 1; - map[s.Name] = new Service { Description = s }; - } - - Services = map; + Source = source.FullName; + ContextDirectory = source.DirectoryName; + Services = services; } - public static Application FromYaml(string path) - { - var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), path)); - - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - var descriptions = deserializer.Deserialize(new StringReader(File.ReadAllText(path))); + public string Source { get; } - var contextDirectory = Path.GetDirectoryName(fullPath); - - foreach (var d in descriptions) - { - if (d.Project == null) - { - continue; - } - - // Try to populate more from launch settings - var projectFilePath = Path.GetFullPath(Path.Combine(contextDirectory, d.Project)); - - if (!TryGetLaunchSettings(projectFilePath, out var projectSettings)) - { - continue; - } - - PopulateFromLaunchSettings(d, projectSettings); - } - - return new Application(descriptions) - { - Source = fullPath, - // Use the file location as the context when loading from a file - ContextDirectory = contextDirectory - }; - } - - public static Application FromProject(string path) - { - var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), path)); - - var projectDescription = CreateDescriptionFromProject(fullPath); - - return new Application(projectDescription == null ? new ServiceDescription[0] : new ServiceDescription[] { projectDescription }) - { - Source = fullPath, - ContextDirectory = Path.GetDirectoryName(fullPath) - }; - } - - private static ServiceDescription CreateDescriptionFromProject(string fullPath) - { - if (!TryGetLaunchSettings(fullPath, out var projectSettings)) - { - return null; - } - - var projectDescription = new ServiceDescription - { - Name = Path.GetFileNameWithoutExtension(fullPath).ToLower(), - Project = fullPath - }; - - PopulateFromLaunchSettings(projectDescription, projectSettings); - - return projectDescription; - } - - private static void PopulateFromLaunchSettings(ServiceDescription projectDescription, JsonElement projectSettings) - { - if (projectDescription.Bindings.Count == 0 && projectSettings.TryGetProperty("applicationUrl", out var applicationUrls)) - { - var addresses = applicationUrls.GetString()?.Split(';'); - - foreach (var address in addresses) - { - var uri = new Uri(address); - - projectDescription.Bindings.Add(new ServiceBinding - { - Port = uri.Port, - Protocol = uri.Scheme - }); - } - } - - if (projectDescription.Configuration.Count == 0 && projectSettings.TryGetProperty("environmentVariables", out var environmentVariables)) - { - foreach (var envVar in environmentVariables.EnumerateObject()) - { - projectDescription.Configuration.Add(new ConfigurationSource - { - Name = envVar.Name, - Value = envVar.Value.GetString() - }); - } - } - - if (projectDescription.Replicas == null && projectSettings.TryGetProperty("replicas", out var replicasElement)) - { - projectDescription.Replicas = replicasElement.GetInt32(); - } - } - - public static Application FromSolution(string path) - { - var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), path)); - - var solution = SolutionFile.Parse(fullPath); - - var descriptions = new List(); - - foreach (var project in solution.ProjectsInOrder) - { - if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat) - { - continue; - } - - var projectFilePath = project.AbsolutePath.Replace('\\', Path.DirectorySeparatorChar); - - var extension = Path.GetExtension(projectFilePath).ToLower(); - switch (extension) - { - case ".csproj": - case ".fsproj": - break; - default: - continue; - } - - var description = CreateDescriptionFromProject(projectFilePath); - - if (description != null) - { - descriptions.Add(description); - } - } - - return new Application(descriptions) - { - Source = fullPath, - ContextDirectory = Path.GetDirectoryName(fullPath) - }; - } + public string ContextDirectory { get; } public Dictionary Services { get; } - internal void PopulateEnvironment(Service service, Action set, string defaultHost = "localhost") + public void PopulateEnvironment(Service service, Action set, string defaultHost = "localhost") { if (service.Description.Configuration != null) { @@ -220,8 +61,8 @@ namespace Micronetes.Hosting.Model if (b.Port != null) { - set($"SERVICE__{configName}__PORT", b.Port.ToString()); - set($"{envName}_SERVICE_PORT", b.Port.ToString()); + set($"SERVICE__{configName}__PORT", b.Port.Value.ToString()); + set($"{envName}_SERVICE_PORT", b.Port.Value.ToString()); } set($"SERVICE__{configName}__HOST", b.Host ?? defaultHost); @@ -237,23 +78,5 @@ namespace Micronetes.Hosting.Model } } } - - private static bool TryGetLaunchSettings(string projectFilePath, out JsonElement projectSettings) - { - var projectDirectory = Path.GetDirectoryName(projectFilePath); - var launchSettingsPath = Path.Combine(projectDirectory, "Properties", "launchSettings.json"); - - if (!File.Exists(launchSettingsPath)) - { - projectSettings = default; - return false; - } - - // If there's a launchSettings.json, then use it to get addresses - var root = JsonSerializer.Deserialize(File.ReadAllText(launchSettingsPath)); - var key = NameSanitizer.SanitizeToIdentifier(Path.GetFileNameWithoutExtension(projectFilePath)); - var profiles = root.GetProperty("profiles"); - return profiles.TryGetProperty(key, out projectSettings); - } } } diff --git a/src/micronetes/Micronetes.Hosting/Model/DockerRunInfo.cs b/src/micronetes/Micronetes.Hosting/Model/DockerRunInfo.cs new file mode 100644 index 00000000..912c0923 --- /dev/null +++ b/src/micronetes/Micronetes.Hosting/Model/DockerRunInfo.cs @@ -0,0 +1,15 @@ +namespace Micronetes.Hosting.Model +{ + public class DockerRunInfo : RunInfo + { + public DockerRunInfo(string image, string? args) + { + Image = image; + Args = args; + } + + public string? Args { get; } + + public string Image { get; } + } +} diff --git a/src/micronetes/Micronetes.Hosting/Model/ExecutableRunInfo.cs b/src/micronetes/Micronetes.Hosting/Model/ExecutableRunInfo.cs new file mode 100644 index 00000000..757f4b50 --- /dev/null +++ b/src/micronetes/Micronetes.Hosting/Model/ExecutableRunInfo.cs @@ -0,0 +1,18 @@ +namespace Micronetes.Hosting.Model +{ + public class ExecutableRunInfo : RunInfo + { + public ExecutableRunInfo(string executable, string? workingDirectory, string? args) + { + Executable = executable; + WorkingDirectory = workingDirectory; + Args = args; + } + + public string Executable { get; } + + public string? WorkingDirectory { get; } + + public string? Args { get; set; } + } +} diff --git a/src/micronetes/Micronetes.Hosting/Model/ProjectRunInfo.cs b/src/micronetes/Micronetes.Hosting/Model/ProjectRunInfo.cs new file mode 100644 index 00000000..8ce36626 --- /dev/null +++ b/src/micronetes/Micronetes.Hosting/Model/ProjectRunInfo.cs @@ -0,0 +1,16 @@ +namespace Micronetes.Hosting.Model +{ + public class ProjectRunInfo : RunInfo + { + public ProjectRunInfo(string project, string? args, bool build) + { + Project = project; + Args = args; + Build = build; + } + + public string? Args { get; } + public bool Build { get; } + public string Project { get; } + } +} diff --git a/src/micronetes/Micronetes.Hosting/Model/RunInfo.cs b/src/micronetes/Micronetes.Hosting/Model/RunInfo.cs new file mode 100644 index 00000000..874e1e7d --- /dev/null +++ b/src/micronetes/Micronetes.Hosting/Model/RunInfo.cs @@ -0,0 +1,6 @@ +namespace Micronetes.Hosting.Model +{ + public abstract class RunInfo + { + } +} diff --git a/src/micronetes/Micronetes.Hosting/Model/Service.cs b/src/micronetes/Micronetes.Hosting/Model/Service.cs index 3056a608..4534fa4d 100644 --- a/src/micronetes/Micronetes.Hosting/Model/Service.cs +++ b/src/micronetes/Micronetes.Hosting/Model/Service.cs @@ -9,8 +9,10 @@ namespace Micronetes.Hosting.Model { public class Service { - public Service() + public Service(ServiceDescription description) { + Description = description; + Logs.Subscribe(entry => { if (CachedLogs.Count > 5000) @@ -22,7 +24,7 @@ namespace Micronetes.Hosting.Model }); } - public ServiceDescription Description { get; set; } + public ServiceDescription Description { get; } public int Restarts { get; set; } @@ -30,17 +32,22 @@ namespace Micronetes.Hosting.Model { get { - if (Description.DockerImage != null) + if (Description.RunInfo is DockerRunInfo) { return ServiceType.Container; } - if (Description.Project != null) + if (Description.RunInfo is ExecutableRunInfo) + { + return ServiceType.Executable; + } + + if (Description.RunInfo is ProjectRunInfo) { return ServiceType.Project; } - return ServiceType.Executable; + return ServiceType.External; } } @@ -86,20 +93,21 @@ namespace Micronetes.Hosting.Model public class ServiceStatus { - public string ProjectFilePath { get; set; } - public string ExecutablePath { get; set; } - public string Args { get; set; } - public string WorkingDirectory { get; set; } + public string? ProjectFilePath { get; set; } + public string? ExecutablePath { get; set; } + public string? Args { get; set; } + public string? WorkingDirectory { get; set; } } public class ProcessStatus : ReplicaStatus { - public ProcessStatus(Service service, string name) : base(service, name) + public ProcessStatus(Service service, string name) + : base(service, name) { } public int? ExitCode { get; set; } public int? Pid { get; set; } - public IDictionary Environment { get; set; } + public IDictionary? Environment { get; set; } } public class DockerStatus : ReplicaStatus @@ -108,11 +116,11 @@ namespace Micronetes.Hosting.Model { } - public string DockerCommand { get; set; } + public string? DockerCommand { get; set; } - public string ContainerId { get; set; } + public string? ContainerId { get; set; } - public int DockerLogsPid { get; set; } + public int? DockerLogsPid { get; set; } } public class ReplicaStatus @@ -127,7 +135,7 @@ namespace Micronetes.Hosting.Model public static JsonConverter JsonConverter = new Converter(); - public IEnumerable Ports { get; set; } + public IEnumerable? Ports { get; set; } [JsonIgnore] public Service Service { get; } @@ -155,6 +163,7 @@ namespace Micronetes.Hosting.Model public enum ServiceType { + External, Project, Executable, Container diff --git a/src/micronetes/Micronetes.Hosting/Model/ServiceBinding.cs b/src/micronetes/Micronetes.Hosting/Model/ServiceBinding.cs index e647898f..5f6b7eee 100644 --- a/src/micronetes/Micronetes.Hosting/Model/ServiceBinding.cs +++ b/src/micronetes/Micronetes.Hosting/Model/ServiceBinding.cs @@ -2,11 +2,11 @@ { public class ServiceBinding { - public string Name { get; set; } - public string ConnectionString { get; set; } + public string? Name { get; set; } + public string? ConnectionString { get; set; } public int? Port { get; set; } public int? InternalPort { get; set; } - public string Host { get; set; } - public string Protocol { get; set; } + public string? Host { get; set; } + public string? Protocol { get; set; } } } diff --git a/src/micronetes/Micronetes.Hosting/Model/ServiceDescription.cs b/src/micronetes/Micronetes.Hosting/Model/ServiceDescription.cs index d5355f42..b680d769 100644 --- a/src/micronetes/Micronetes.Hosting/Model/ServiceDescription.cs +++ b/src/micronetes/Micronetes.Hosting/Model/ServiceDescription.cs @@ -1,29 +1,33 @@ using System.Collections.Generic; -using System.Linq; -using YamlDotNet.Serialization; namespace Micronetes.Hosting.Model { public class ServiceDescription { - public string Name { get; set; } - public bool External { get; set; } - public string DockerImage { get; set; } - public string Project { get; set; } - public bool? Build { get; set; } = true; - public string Executable { get; set; } - public string WorkingDirectory { get; set; } - public string Args { get; set; } - public int? Replicas { get; set; } - public List Bindings { get; set; } = new List(); - [YamlMember(Alias = "env")] - public List Configuration { get; set; } = new List(); + public ServiceDescription(string name, RunInfo? runInfo) + { + Name = name; + RunInfo = runInfo; + } + + public string Name { get; } + + public RunInfo? RunInfo { get; } + + public int Replicas { get; set; } = 1; + public List Bindings { get; } = new List(); + public List Configuration { get; } = new List(); } public class ConfigurationSource { - public string Name { get; set; } - public string Value { get; set; } - public string Source { get; set; } + public ConfigurationSource(string name, string value) + { + Name = name; + Value = value; + } + + public string Name { get; } + public string Value { get; } } } diff --git a/src/micronetes/Micronetes.Hosting/ProcessRunner.cs b/src/micronetes/Micronetes.Hosting/ProcessRunner.cs index 6df8f914..5c80f231 100644 --- a/src/micronetes/Micronetes.Hosting/ProcessRunner.cs +++ b/src/micronetes/Micronetes.Hosting/ProcessRunner.cs @@ -29,7 +29,16 @@ namespace Micronetes.Hosting var index = 0; foreach (var s in application.Services) { - tasks[index++] = s.Value.Description.External ? Task.CompletedTask : LaunchService(application, s.Value); + tasks[index++] = s.Value.ServiceType switch + { + ServiceType.Container => Task.CompletedTask, + ServiceType.External => Task.CompletedTask, + + ServiceType.Executable => LaunchService(application, s.Value), + ServiceType.Project => LaunchService(application, s.Value), + + _ => throw new InvalidOperationException("Unknown ServiceType."), + }; } return Task.WhenAll(tasks); @@ -43,33 +52,33 @@ namespace Micronetes.Hosting private async Task LaunchService(Application application, Service service) { var serviceDescription = service.Description; - - if (serviceDescription.DockerImage != null) - { - return; - } - var serviceName = serviceDescription.Name; var path = ""; var workingDirectory = ""; - var args = service.Description.Args ?? ""; + var args = ""; - if (serviceDescription.Project != null) + if (serviceDescription.RunInfo is ProjectRunInfo project) { - var expandedProject = Environment.ExpandEnvironmentVariables(serviceDescription.Project); + var expandedProject = Environment.ExpandEnvironmentVariables(project.Project); var fullProjectPath = Path.GetFullPath(Path.Combine(application.ContextDirectory, expandedProject)); path = GetExePath(fullProjectPath); - workingDirectory = Path.GetDirectoryName(fullProjectPath); + workingDirectory = Path.GetDirectoryName(fullProjectPath)!; + args = project.Args ?? ""; service.Status.ProjectFilePath = fullProjectPath; } - else + else if (serviceDescription.RunInfo is ExecutableRunInfo executable) { - var expandedExecutable = Environment.ExpandEnvironmentVariables(serviceDescription.Executable); + var expandedExecutable = Environment.ExpandEnvironmentVariables(executable.Executable); path = Path.GetFullPath(Path.Combine(application.ContextDirectory, expandedExecutable)); - workingDirectory = serviceDescription.WorkingDirectory != null ? - Path.GetFullPath(Path.Combine(application.ContextDirectory, Environment.ExpandEnvironmentVariables(serviceDescription.WorkingDirectory))) : - Path.GetDirectoryName(path); + workingDirectory = executable.WorkingDirectory != null ? + Path.GetFullPath(Path.Combine(application.ContextDirectory, Environment.ExpandEnvironmentVariables(executable.WorkingDirectory))) : + Path.GetDirectoryName(path)!; + args = executable.Args ?? ""; + } + else + { + throw new InvalidOperationException("Unsupported ServiceType."); } // If this is a dll then use dotnet to run it @@ -83,12 +92,11 @@ namespace Micronetes.Hosting service.Status.WorkingDirectory = workingDirectory; service.Status.Args = args; - var processInfo = new ProcessInfo - { - Tasks = new Task[service.Description.Replicas.Value] - }; - - if (service.Status.ProjectFilePath != null && service.Description.Build.GetValueOrDefault() && _buildProjects) + var processInfo = new ProcessInfo(new Task[service.Description.Replicas]); + if (service.Status.ProjectFilePath != null && + service.Description.RunInfo is ProjectRunInfo project2 && + project2.Build && + _buildProjects) { // Sometimes building can fail because of file locking (like files being open in VS) _logger.LogInformation("Building project {ProjectFile}", service.Status.ProjectFilePath); @@ -106,7 +114,7 @@ namespace Micronetes.Hosting } } - async Task RunApplicationAsync(IEnumerable<(int Port, int BindingPort, string Protocol)> ports) + async Task RunApplicationAsync(IEnumerable<(int Port, int BindingPort, string? Protocol)> ports) { var hasPorts = ports.Any(); @@ -215,7 +223,7 @@ namespace Micronetes.Hosting // port for (int i = 0; i < serviceDescription.Replicas; i++) { - var ports = new List<(int, int, string)>(); + var ports = new List<(int, int, string?)>(); foreach (var binding in serviceDescription.Bindings) { if (binding.Port == null) @@ -233,7 +241,7 @@ namespace Micronetes.Hosting { for (int i = 0; i < service.Description.Replicas; i++) { - processInfo.Tasks[i] = RunApplicationAsync(Enumerable.Empty<(int, int, string)>()); + processInfo.Tasks[i] = RunApplicationAsync(Enumerable.Empty<(int, int, string?)>()); } } @@ -270,7 +278,7 @@ namespace Micronetes.Hosting var outputFileName = Path.GetFileNameWithoutExtension(projectFilePath) + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""); - var debugOutputPath = Path.Combine(Path.GetDirectoryName(projectFilePath), "bin", "Debug"); + var debugOutputPath = Path.Combine(Path.GetDirectoryName(projectFilePath)!, "bin", "Debug"); var tfms = Directory.Exists(debugOutputPath) ? Directory.GetDirectories(debugOutputPath) : Array.Empty(); @@ -292,9 +300,15 @@ namespace Micronetes.Hosting private class ProcessInfo { - public Task[] Tasks { get; set; } - public CancellationTokenSource StoppedTokenSource { get; set; } = new CancellationTokenSource(); + public ProcessInfo(Task[] tasks) + { + Tasks = tasks; + } + + public Task[] Tasks { get; } + + public CancellationTokenSource StoppedTokenSource { get; } = new CancellationTokenSource(); } } } diff --git a/src/micronetes/Micronetes.Hosting/ProxyService.cs b/src/micronetes/Micronetes.Hosting/ProxyService.cs index 06d60f10..b5b538dc 100644 --- a/src/micronetes/Micronetes.Hosting/ProxyService.cs +++ b/src/micronetes/Micronetes.Hosting/ProxyService.cs @@ -18,7 +18,7 @@ namespace Micronetes.Hosting { public class ProxyService : IApplicationProcessor { - private IHost _host; + private IHost? _host; private readonly ILogger _logger; public ProxyService(ILogger logger) @@ -35,7 +35,7 @@ namespace Micronetes.Hosting { foreach (var service in application.Services.Values) { - if (service.Description.External) + if (service.Description.RunInfo == null) { // We eventually want to proxy everything, this is temporary continue; @@ -69,7 +69,7 @@ namespace Micronetes.Hosting var ports = new List(); - for (int i = 0; i < service.Description.Replicas; i++) + for (var i = 0; i < service.Description.Replicas; i++) { // Reserve a port for each replica var port = GetNextPort(); @@ -92,7 +92,7 @@ namespace Micronetes.Hosting var next = (int)(Interlocked.Increment(ref count) % ports.Count); - NetworkStream targetStream = null; + NetworkStream? targetStream = null; try { @@ -114,7 +114,10 @@ namespace Micronetes.Hosting { _logger.LogDebug(ex, "Proxy error for service {ServiceName}", service.Description.Name); - await targetStream.DisposeAsync(); + if (targetStream is object) + { + await targetStream.DisposeAsync(); + } connection.Abort(); return; @@ -171,7 +174,10 @@ namespace Micronetes.Hosting { await _host.StopAsync(); - await (_host as IAsyncDisposable).DisposeAsync(); + if (_host is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } } } } diff --git a/src/tye/ConfigModel/ConfigApplication.cs b/src/tye/ConfigModel/ConfigApplication.cs new file mode 100644 index 00000000..6716748f --- /dev/null +++ b/src/tye/ConfigModel/ConfigApplication.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Micronetes.Hosting.Model; +using YamlDotNet.Serialization; + +namespace Tye.ConfigModel +{ + internal class ConfigApplication + { + // This gets set by all of the code paths that read the application + [YamlIgnore] + public FileInfo Source { get; set; } = default!; + + public string? Name { get; set; } + + public string? Registry { get; set; } + + public List Services { get; set; } = new List(); + + public Application ToHostingApplication() + { + var services = new Dictionary(); + foreach (var service in Services) + { + RunInfo? runInfo; + if (service.External) + { + runInfo = null; + } + else if (service.DockerImage is object) + { + runInfo = new DockerRunInfo(service.DockerImage, service.Args); + } + else if (service.Executable is object) + { + runInfo = new ExecutableRunInfo(service.Executable, service.WorkingDirectory, service.Args); + } + else if (service.Project is object) + { + runInfo = new ProjectRunInfo(service.Project, service.Args, service.Build ?? true); + } + else + { + throw new InvalidOperationException($"Cannot figure out how to run service '{service.Name}'."); + } + + var description = new ServiceDescription(service.Name, runInfo) + { + Replicas = service.Replicas ?? 1, + }; + + foreach (var binding in service.Bindings) + { + description.Bindings.Add(new ServiceBinding() + { + ConnectionString = binding.ConnectionString, + Host = binding.Host, + InternalPort = binding.InternalPort, + Name = binding.Name, + Port = binding.Port, + Protocol = binding.Protocol, + }); + } + + foreach (var entry in service.Configuration) + { + description.Configuration.Add(new ConfigurationSource(entry.Name, entry.Value)); + } + + services.Add(service.Name, new Service(description)); + } + + return new Application(Source, services); + } + } +} diff --git a/src/tye/ConfigModel/ConfigConfigurationSource.cs b/src/tye/ConfigModel/ConfigConfigurationSource.cs new file mode 100644 index 00000000..cafda2e0 --- /dev/null +++ b/src/tye/ConfigModel/ConfigConfigurationSource.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tye.ConfigModel +{ + internal class ConfigConfigurationSource + { + [Required] + public string Name { get; set; } = default!; + [Required] + public string Value { get; set; } = default!; + public string? Source { get; set; } + } +} diff --git a/src/tye/ConfigModel/ConfigFactory.cs b/src/tye/ConfigModel/ConfigFactory.cs new file mode 100644 index 00000000..e1a98889 --- /dev/null +++ b/src/tye/ConfigModel/ConfigFactory.cs @@ -0,0 +1,176 @@ +using System; +using System.IO; +using System.Text.Json; +using Microsoft.Build.Construction; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Tye.ConfigModel +{ + internal static class ConfigFactory + { + public static ConfigApplication FromFile(FileInfo file) + { + var extension = file.Extension.ToLowerInvariant(); + switch (extension) + { + case ".yaml": + case ".yml": + return FromYaml(file); + + case ".csproj": + case ".fsproj": + return FromProject(file); + + case ".sln": + return FromSolution(file); + + default: + throw new CommandException($"File '{file.FullName}' is not a supported format."); + } + } + + private static ConfigApplication FromProject(FileInfo file) + { + var application = new ConfigApplication() + { + Source = file, + }; + + var service = CreateService(file); + if (service is object) + { + application.Services.Add(service); + } + + return application; + } + + private static ConfigApplication FromSolution(FileInfo file) + { + var application = new ConfigApplication() + { + Source = file, + }; + + var solution = SolutionFile.Parse(file.FullName); + foreach (var project in solution.ProjectsInOrder) + { + if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat) + { + continue; + } + + var projectFilePath = project.AbsolutePath.Replace('\\', Path.DirectorySeparatorChar); + var extension = Path.GetExtension(projectFilePath).ToLower(); + switch (extension) + { + case ".csproj": + case ".fsproj": + break; + default: + continue; + } + + var description = CreateService(new FileInfo(projectFilePath)); + if (description != null) + { + application.Services.Add(description); + } + } + + return application; + } + + private static ConfigApplication FromYaml(FileInfo file) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + using var reader = file.OpenText(); + var application = deserializer.Deserialize(reader); + application.Source = file; + + foreach (var service in application.Services) + { + if (service.Project == null) + { + continue; + } + + if (!TryGetLaunchProfile(new FileInfo(Path.Combine(file.DirectoryName, service.Project)), out var launchProfile)) + { + continue; + } + + PopulateFromLaunchProfile(service, launchProfile); + } + + return application; + } + + private static bool TryGetLaunchProfile(FileInfo file, out JsonElement launchProfile) + { + var launchSettingsPath = Path.Combine(file.DirectoryName, "Properties", "launchSettings.json"); + if (!File.Exists(launchSettingsPath)) + { + launchProfile = default; + return false; + } + + // If there's a launchSettings.json, then use it to get addresses + var root = JsonSerializer.Deserialize(File.ReadAllText(launchSettingsPath)); + var key = NameSanitizer.SanitizeToIdentifier(Path.GetFileNameWithoutExtension(file.Name)); + var profiles = root.GetProperty("profiles"); + return profiles.TryGetProperty(key, out launchProfile); + } + + private static ConfigService? CreateService(FileInfo file) + { + if (!TryGetLaunchProfile(file, out var launchProfile)) + { + return null; + } + + var service = new ConfigService() + { + Name = Path.GetFileNameWithoutExtension(file.Name).ToLowerInvariant(), + Project = file.FullName, + }; + + PopulateFromLaunchProfile(service, launchProfile); + + return service; + } + + private static void PopulateFromLaunchProfile(ConfigService service, JsonElement launchProfile) + { + if (service.Bindings.Count == 0 && launchProfile.TryGetProperty("applicationUrl", out var applicationUrls)) + { + var addresses = applicationUrls.GetString().Split(';', StringSplitOptions.RemoveEmptyEntries); + foreach (var address in addresses) + { + var uri = new Uri(address); + service.Bindings.Add(new ConfigServiceBinding() + { + Port = uri.Port, + Protocol = uri.Scheme + }); + } + } + + if (service.Configuration.Count == 0 && launchProfile.TryGetProperty("environmentVariables", out var environmentVariables)) + { + foreach (var envVar in environmentVariables.EnumerateObject()) + { + service.Configuration.Add(new ConfigConfigurationSource() + { + Name = envVar.Name, + Value = envVar.Value.GetString() + }); + } + } + } + } +} diff --git a/src/tye/ConfigModel/ConfigService.cs b/src/tye/ConfigModel/ConfigService.cs new file mode 100644 index 00000000..d45d4e75 --- /dev/null +++ b/src/tye/ConfigModel/ConfigService.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using YamlDotNet.Serialization; + +namespace Tye.ConfigModel +{ + internal class ConfigService + { + [Required] + public string Name { get; set; } = default!; + public bool External { get; set; } + public string? DockerImage { get; set; } + public string? Project { get; set; } + public bool? Build { get; set; } + public string? Executable { get; set; } + public string? WorkingDirectory { get; set; } + public string? Args { get; set; } + public int? Replicas { get; set; } + public List Bindings { get; set; } = new List(); + + [YamlMember(Alias = "env")] + public List Configuration { get; set; } = new List(); + } +} diff --git a/src/tye/ConfigModel/ConfigServiceBinding.cs b/src/tye/ConfigModel/ConfigServiceBinding.cs new file mode 100644 index 00000000..d8cd793d --- /dev/null +++ b/src/tye/ConfigModel/ConfigServiceBinding.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tye.ConfigModel +{ + internal class ConfigServiceBinding + { + [Required] + public string Name { get; set; } = default!; + public string? ConnectionString { get; set; } + public int? Port { get; set; } + public int? InternalPort { get; set; } + public string? Host { get; set; } + public string? Protocol { get; set; } + } +} diff --git a/src/tye/OpulenceApplicationAdapter.cs b/src/tye/OpulenceApplicationAdapter.cs index cc81112d..b03c16ee 100644 --- a/src/tye/OpulenceApplicationAdapter.cs +++ b/src/tye/OpulenceApplicationAdapter.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; using Opulence; +using Tye.ConfigModel; namespace Tye { internal class OpulenceApplicationAdapter : Opulence.Application { - private readonly Micronetes.Hosting.Model.Application application; + private readonly ConfigApplication application; public OpulenceApplicationAdapter( - Micronetes.Hosting.Model.Application application, + ConfigApplication application, ApplicationGlobals globals, IReadOnlyList services) { @@ -19,7 +20,7 @@ namespace Tye public override ApplicationGlobals Globals { get; } - public override string RootDirectory => application.ContextDirectory; + public override string RootDirectory => application.Source.DirectoryName; public override IReadOnlyList Services { get; } } diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index 748a4ec1..7186414a 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -6,6 +6,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; using Opulence; +using Tye.ConfigModel; namespace Tye { @@ -28,28 +29,32 @@ namespace Tye throw new CommandException("No project or solution file was found."); } - var application = ResolveApplication(path); + var application = ConfigFactory.FromFile(path); return ExecuteAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); }); return command; } - private static async Task ExecuteAsync(OutputContext output, Micronetes.Hosting.Model.Application application, string environment, bool interactive) + private static async Task ExecuteAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) { - var globals = new ApplicationGlobals(); - var services = new List(); + var globals = new ApplicationGlobals() + { + Name = application.Name, + Registry = application.Registry is null ? null : new ContainerRegistry(application.Registry), + }; - foreach (var kvp in application.Services) + var services = new List(); + foreach (var configService in application.Services) { - if (kvp.Value.Description.Project is string projectFile) + if (configService.Project is string projectFile) { var project = new Project(projectFile); - var service = new Service(kvp.Key) + var service = new Service(configService.Name) { Source = project, }; - var serviceEntry = new ServiceEntry(service, kvp.Key); + var serviceEntry = new ServiceEntry(service, configService.Name); await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(projectFile), project); @@ -86,25 +91,15 @@ namespace Tye }; steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, }); - - // If this is command is for a project, then deploy the component manifest - // for just the project. We won't run the "application deploy" part. - if (!string.Equals(".csproj", Path.GetExtension(application.Source), StringComparison.Ordinal) && - !string.Equals(".fsproj", Path.GetExtension(application.Source), StringComparison.Ordinal)) - { - steps.Add(new DeployServiceYamlStep() { Environment = environment, }); - } + steps.Add(new DeployServiceYamlStep() { Environment = environment, }); var executor = new ServiceExecutor(output, opulenceApplication, steps); foreach (var service in opulenceApplication.Services) { - if (service.IsMatchForProject(opulenceApplication, new FileInfo(application.Source))) - { - await executor.ExecuteAsync(service); - } + await executor.ExecuteAsync(service); } - await PackageApplicationAsync(output, opulenceApplication, Path.GetDirectoryName(application.Source), environment); + await PackageApplicationAsync(output, opulenceApplication, application.Source.Directory.Name, environment); } private static async Task PackageApplicationAsync(OutputContext output, Opulence.Application application, string applicationName, string environment) diff --git a/src/tye/Program.InitCommand.cs b/src/tye/Program.InitCommand.cs index 2f80a543..dd7c8696 100644 --- a/src/tye/Program.InitCommand.cs +++ b/src/tye/Program.InitCommand.cs @@ -3,8 +3,7 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.CommandLine.IO; using System.IO; -using System.Linq; -using Micronetes.Hosting.Model; +using Tye.ConfigModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -29,7 +28,17 @@ namespace Tye throw new CommandException($"File '{path.FullName}' already exists."); } - var template = @"- name: app + 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 @@ -39,11 +48,15 @@ namespace Tye # 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 = ResolveApplication(path); + var application = ConfigFactory.FromFile(path); var serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) @@ -51,54 +64,43 @@ namespace Tye var extension = path.Extension.ToLowerInvariant(); var directory = path.Directory; - var descriptions = application.Services.Select(s => s.Value.Description).ToList(); // Clear all bindings if any for solutions and project files if (extension == ".sln" || extension == ".csproj" || extension == ".fsproj") { - foreach (var d in descriptions) + // 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) { - d.Bindings = null; - d.Replicas = null; - d.Build = null; - d.Configuration = null; - d.Project = d.Project.Substring(directory.FullName.Length).TrimStart(Path.DirectorySeparatorChar); + 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 = serializer.Serialize(descriptions); + template = @" +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +".TrimStart() + serializer.Serialize(application); } - File.WriteAllText("tye.yaml", template); - console.Out.WriteLine("Created \"tye.yaml\""); + File.WriteAllText(outputFilePath, template); + console.Out.WriteLine($"Created '{outputFilePath}'."); }); return command; } - - private static Application ResolveApplication(FileInfo file) - { - if (!file.Exists) - { - throw new FileNotFoundException($"File '{file.FullName}' does not exist"); - } - - switch (file.Extension.ToLower()) - { - case ".yaml": - case ".yml": - return Application.FromYaml(file.FullName); - - case ".csproj": - case ".fsproj": - return Application.FromProject(file.FullName); - - case ".sln": - return Application.FromSolution(file.FullName); - - default: - throw new NotSupportedException($"File '{file.FullName}' is not a supported format."); - } - } } } diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index 4a11162e..fdfdc7ee 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -3,6 +3,7 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.IO; using Micronetes.Hosting; +using Tye.ConfigModel; namespace Tye { @@ -57,8 +58,8 @@ namespace Tye throw new CommandException("No project or solution file was found."); } - var application = ResolveApplication(path); - return MicronetesHost.RunAsync(application, args); + var application = ConfigFactory.FromFile(path); + return MicronetesHost.RunAsync(application.ToHostingApplication(), args); }); return command;