Browse Source

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
davidfowl/dependencies
Ryan Nowak 7 years ago
parent
commit
d0773bcd52
  1. 25
      src/micronetes/Micronetes.Hosting.Diagnostics/DiagnosticsCollector.cs
  2. 8
      src/micronetes/Micronetes.Hosting/Dashboard/Pages/Index.razor
  3. 6
      src/micronetes/Micronetes.Hosting/Dashboard/Pages/Logs.razor
  4. 6
      src/micronetes/Micronetes.Hosting/Dashboard/Pages/ServiceDetails.razor
  5. 2
      src/micronetes/Micronetes.Hosting/Dashboard/Shared/NavMenu.razor
  6. 37
      src/micronetes/Micronetes.Hosting/DockerRunner.cs
  7. 19
      src/micronetes/Micronetes.Hosting/EventPipeDiagnosticsRunner.cs
  8. 12
      src/micronetes/Micronetes.Hosting/Infrastructure/ProcessUtil.cs
  9. 2
      src/micronetes/Micronetes.Hosting/Micronetes.Hosting.csproj
  10. 2
      src/micronetes/Micronetes.Hosting/MicronetesHost.cs
  11. 195
      src/micronetes/Micronetes.Hosting/Model/Application.cs
  12. 15
      src/micronetes/Micronetes.Hosting/Model/DockerRunInfo.cs
  13. 18
      src/micronetes/Micronetes.Hosting/Model/ExecutableRunInfo.cs
  14. 16
      src/micronetes/Micronetes.Hosting/Model/ProjectRunInfo.cs
  15. 6
      src/micronetes/Micronetes.Hosting/Model/RunInfo.cs
  16. 39
      src/micronetes/Micronetes.Hosting/Model/Service.cs
  17. 8
      src/micronetes/Micronetes.Hosting/Model/ServiceBinding.cs
  18. 38
      src/micronetes/Micronetes.Hosting/Model/ServiceDescription.cs
  19. 70
      src/micronetes/Micronetes.Hosting/ProcessRunner.cs
  20. 18
      src/micronetes/Micronetes.Hosting/ProxyService.cs
  21. 77
      src/tye/ConfigModel/ConfigApplication.cs
  22. 13
      src/tye/ConfigModel/ConfigConfigurationSource.cs
  23. 176
      src/tye/ConfigModel/ConfigFactory.cs
  24. 24
      src/tye/ConfigModel/ConfigService.cs
  25. 15
      src/tye/ConfigModel/ConfigServiceBinding.cs
  26. 7
      src/tye/OpulenceApplicationAdapter.cs
  27. 37
      src/tye/Program.DeployCommand.cs
  28. 82
      src/tye/Program.InitCommand.cs
  29. 5
      src/tye/Program.RunCommand.cs

25
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<string, string> metrics,
CancellationToken cancellationToken)
public void ProcessEvents(
string applicationName,
string serviceName,
int processId,
string replicaName,
IDictionary<string, string> 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<Guid, LogActivityItem>();
var stack = new Stack<Guid>();
@ -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<string, object>)traceEvent.PayloadValue(0);
var eventPayload = (IDictionary<string, object>)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)
{

8
src/micronetes/Micronetes.Hosting/Dashboard/Pages/Index.razor

@ -26,13 +26,13 @@
@service.ServiceType
</td>
<td>
@if (service.Description.Project != null)
@if (service.Description.RunInfo is ProjectRunInfo project)
{
<p>@service.Description.Project</p>
<p>@project.Project</p>
}
else if (service.Description.DockerImage != null)
else if (service.Description.RunInfo is DockerRunInfo docker)
{
<p>@service.Description.DockerImage</p>
<p>@docker.Image</p>
}
</td>
<td>

6
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()
{

6
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()
{

2
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()
{

37
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();
}
}
}

19
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();
}
}

12
src/micronetes/Micronetes.Hosting/Infrastructure/ProcessUtil.cs

@ -14,12 +14,12 @@ namespace Micronetes.Hosting
public static async Task<ProcessResult> RunAsync(
string filename,
string arguments,
string workingDirectory = null,
string? workingDirectory = null,
bool throwOnError = true,
IDictionary<string, string> environmentVariables = null,
Action<string> outputDataReceived = null,
Action<string> errorDataReceived = null,
Action<int> onStart = null,
IDictionary<string, string>? environmentVariables = null,
Action<string>? outputDataReceived = null,
Action<string>? errorDataReceived = null,
Action<int>? 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<object>();
var cancelledTcs = new TaskCompletionSource<object?>();
using var _ = cancellationToken.Register(() => cancelledTcs.TrySetResult(null));
var result = await Task.WhenAny(processLifetimeTask.Task, cancelledTcs.Task);

2
src/micronetes/Micronetes.Hosting/Micronetes.Hosting.csproj

@ -6,7 +6,7 @@
<Description>Orchestration host APIs.</Description>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<Nullable>disable</Nullable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

2
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<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var waitForStop = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
lifetime.ApplicationStopping.Register(obj => waitForStop.TrySetResult(null), null);
await waitForStop.Task;

195
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<ServiceDescription> services)
public Application(FileInfo source, Dictionary<string, Service> services)
{
var map = new Dictionary<string, Service>();
// 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<ServiceDescription[]>(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<ServiceDescription>();
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<string, Service> Services { get; }
internal void PopulateEnvironment(Service service, Action<string, string> set, string defaultHost = "localhost")
public void PopulateEnvironment(Service service, Action<string, string> 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<JsonElement>(File.ReadAllText(launchSettingsPath));
var key = NameSanitizer.SanitizeToIdentifier(Path.GetFileNameWithoutExtension(projectFilePath));
var profiles = root.GetProperty("profiles");
return profiles.TryGetProperty(key, out projectSettings);
}
}
}

15
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; }
}
}

18
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; }
}
}

16
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; }
}
}

6
src/micronetes/Micronetes.Hosting/Model/RunInfo.cs

@ -0,0 +1,6 @@
namespace Micronetes.Hosting.Model
{
public abstract class RunInfo
{
}
}

39
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<string, string> Environment { get; set; }
public IDictionary<string, string>? 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<ReplicaStatus> JsonConverter = new Converter();
public IEnumerable<int> Ports { get; set; }
public IEnumerable<int>? Ports { get; set; }
[JsonIgnore]
public Service Service { get; }
@ -155,6 +163,7 @@ namespace Micronetes.Hosting.Model
public enum ServiceType
{
External,
Project,
Executable,
Container

8
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; }
}
}

38
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<ServiceBinding> Bindings { get; set; } = new List<ServiceBinding>();
[YamlMember(Alias = "env")]
public List<ConfigurationSource> Configuration { get; set; } = new List<ConfigurationSource>();
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<ServiceBinding> Bindings { get; } = new List<ServiceBinding>();
public List<ConfigurationSource> Configuration { get; } = new List<ConfigurationSource>();
}
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; }
}
}

70
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<string>();
@ -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();
}
}
}

18
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<int>();
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();
}
}
}
}

77
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<ConfigService> Services { get; set; } = new List<ConfigService>();
public Application ToHostingApplication()
{
var services = new Dictionary<string, Service>();
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);
}
}
}

13
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; }
}
}

176
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<ConfigApplication>(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<JsonElement>(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()
});
}
}
}
}
}

24
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<ConfigServiceBinding> Bindings { get; set; } = new List<ConfigServiceBinding>();
[YamlMember(Alias = "env")]
public List<ConfigConfigurationSource> Configuration { get; set; } = new List<ConfigConfigurationSource>();
}
}

15
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; }
}
}

7
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<ServiceEntry> 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<ServiceEntry> Services { get; }
}

37
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<Opulence.ServiceEntry>();
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<Opulence.ServiceEntry>();
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)

82
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.");
}
}
}
}

5
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;

Loading…
Cancel
Save