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.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; } |
|||
} |
|||
} |
|||
|
|||
@ -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