Browse Source
- 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 scopedavidfowl/dependencies
29 changed files with 595 additions and 383 deletions
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Micronetes.Hosting.Model |
||||
|
{ |
||||
|
public abstract class RunInfo |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -1,29 +1,33 @@ |
|||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.Linq; |
|
||||
using YamlDotNet.Serialization; |
|
||||
|
|
||||
namespace Micronetes.Hosting.Model |
namespace Micronetes.Hosting.Model |
||||
{ |
{ |
||||
public class ServiceDescription |
public class ServiceDescription |
||||
{ |
{ |
||||
public string Name { get; set; } |
public ServiceDescription(string name, RunInfo? runInfo) |
||||
public bool External { get; set; } |
{ |
||||
public string DockerImage { get; set; } |
Name = name; |
||||
public string Project { get; set; } |
RunInfo = runInfo; |
||||
public bool? Build { get; set; } = true; |
} |
||||
public string Executable { get; set; } |
|
||||
public string WorkingDirectory { get; set; } |
public string Name { get; } |
||||
public string Args { get; set; } |
|
||||
public int? Replicas { get; set; } |
public RunInfo? RunInfo { get; } |
||||
public List<ServiceBinding> Bindings { get; set; } = new List<ServiceBinding>(); |
|
||||
[YamlMember(Alias = "env")] |
public int Replicas { get; set; } = 1; |
||||
public List<ConfigurationSource> Configuration { get; set; } = new List<ConfigurationSource>(); |
public List<ServiceBinding> Bindings { get; } = new List<ServiceBinding>(); |
||||
|
public List<ConfigurationSource> Configuration { get; } = new List<ConfigurationSource>(); |
||||
} |
} |
||||
|
|
||||
public class ConfigurationSource |
public class ConfigurationSource |
||||
{ |
{ |
||||
public string Name { get; set; } |
public ConfigurationSource(string name, string value) |
||||
public string Value { get; set; } |
{ |
||||
public string Source { get; set; } |
Name = name; |
||||
|
Value = value; |
||||
|
} |
||||
|
|
||||
|
public string Name { get; } |
||||
|
public string Value { get; } |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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() |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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>(); |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue