mirror of https://github.com/dotnet/tye.git
Browse Source
Merged the data model we use for m8s features with the features that come from rynowak/opulence. The `ApplicationBuilder` type (and related types) now capture all of the details we use for everything. There's a lot of just moving stuff around in this change - the goal is to have a separate data model for: - The config we read - The representation we mutate - The data model used by Tye.Hosting So in this PR there's a lot of righthand/lefthand.pull/182/head
62 changed files with 927 additions and 873 deletions
@ -1,231 +0,0 @@ |
|||||
// Licensed to the .NET Foundation under one or more agreements.
|
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
|
||||
// See the LICENSE file in the project root for more information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.IO; |
|
||||
using System.Linq; |
|
||||
using System.Reflection; |
|
||||
|
|
||||
namespace Microsoft.Tye |
|
||||
{ |
|
||||
public abstract class Application |
|
||||
{ |
|
||||
public abstract ApplicationGlobals Globals { get; } |
|
||||
|
|
||||
public abstract string RootDirectory { get; } |
|
||||
|
|
||||
public abstract IReadOnlyList<ServiceEntry> Services { get; } |
|
||||
|
|
||||
public string GetProjectDirectory(Project project) |
|
||||
{ |
|
||||
if (project is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(project)); |
|
||||
} |
|
||||
|
|
||||
return Path.GetDirectoryName(Path.Combine(RootDirectory, project.RelativeFilePath))!; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public class ServiceEntry |
|
||||
{ |
|
||||
public ServiceEntry(Service service, string friendlyName, IEnumerable<string>? environments = null) |
|
||||
{ |
|
||||
if (service is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(service)); |
|
||||
} |
|
||||
|
|
||||
if (friendlyName is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(friendlyName)); |
|
||||
} |
|
||||
|
|
||||
Service = service; |
|
||||
FriendlyName = friendlyName; |
|
||||
|
|
||||
Environments = environments?.ToArray() ?? Array.Empty<string>(); |
|
||||
} |
|
||||
|
|
||||
public IReadOnlyList<string> Environments { get; } |
|
||||
|
|
||||
public string FriendlyName { get; } |
|
||||
|
|
||||
public Service Service { get; } |
|
||||
|
|
||||
public List<ServiceOutput> Outputs { get; } = new List<ServiceOutput>(); |
|
||||
|
|
||||
public bool AppliesToEnvironment(string environment) |
|
||||
{ |
|
||||
if (environment is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(environment)); |
|
||||
} |
|
||||
|
|
||||
return Environments.Count == 0 || Environments.Contains(environment, StringComparer.OrdinalIgnoreCase); |
|
||||
} |
|
||||
|
|
||||
public bool IsMatchForProject(Application application, FileInfo projectFile) |
|
||||
{ |
|
||||
if (application is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(application)); |
|
||||
} |
|
||||
|
|
||||
if (projectFile is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(projectFile)); |
|
||||
} |
|
||||
|
|
||||
if (string.Equals(projectFile.Extension, ".sln", StringComparison.Ordinal)) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
if (Service.Source is Project project && |
|
||||
string.Equals(application.GetProjectDirectory(project), projectFile.DirectoryName, StringComparison.Ordinal)) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public abstract class ServiceOutput |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public class DockerImageOutput : ServiceOutput |
|
||||
{ |
|
||||
public DockerImageOutput(string imageName, string imageTag) |
|
||||
{ |
|
||||
if (imageName is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(imageName)); |
|
||||
} |
|
||||
|
|
||||
if (imageTag is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(imageTag)); |
|
||||
} |
|
||||
|
|
||||
ImageName = imageName; |
|
||||
ImageTag = imageTag; |
|
||||
} |
|
||||
|
|
||||
public string ImageName { get; } |
|
||||
|
|
||||
public string ImageTag { get; } |
|
||||
} |
|
||||
|
|
||||
|
|
||||
internal class ApplicationWrapper : Application |
|
||||
{ |
|
||||
private readonly object inner; |
|
||||
private ApplicationGlobals? globals; |
|
||||
private List<ServiceEntry>? services; |
|
||||
|
|
||||
public ApplicationWrapper(object inner, string rootDirectory) |
|
||||
{ |
|
||||
if (inner is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(inner)); |
|
||||
} |
|
||||
|
|
||||
if (rootDirectory is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(rootDirectory)); |
|
||||
} |
|
||||
|
|
||||
this.inner = inner; |
|
||||
RootDirectory = rootDirectory; |
|
||||
} |
|
||||
|
|
||||
public override ApplicationGlobals Globals |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
if (globals == null) |
|
||||
{ |
|
||||
var property = inner.GetType().GetProperty("Globals", BindingFlags.Instance | BindingFlags.Public); |
|
||||
if (property != null && property.PropertyType == typeof(ApplicationGlobals)) |
|
||||
{ |
|
||||
globals = (ApplicationGlobals?)property.GetValue(inner, null); |
|
||||
} |
|
||||
|
|
||||
globals ??= new ApplicationGlobals(); |
|
||||
} |
|
||||
|
|
||||
return globals!; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public override string RootDirectory { get; } |
|
||||
|
|
||||
public override IReadOnlyList<ServiceEntry> Services |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
if (services == null) |
|
||||
{ |
|
||||
services = new List<ServiceEntry>(); |
|
||||
var properties = inner.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); |
|
||||
for (var i = 0; i < properties.Length; i++) |
|
||||
{ |
|
||||
var property = properties[i]; |
|
||||
if (property.PropertyType == typeof(Service)) |
|
||||
{ |
|
||||
var environments = new List<string>(); |
|
||||
var environmentAttributes = property.GetCustomAttributes<EnvironmentAttribute>(); |
|
||||
environments.AddRange(environmentAttributes.Select(e => e.EnvironmentName)); |
|
||||
|
|
||||
var value = property.GetValue(inner, null); |
|
||||
if (value == null) |
|
||||
{ |
|
||||
throw new InvalidOperationException("Service properties must return a non-null value."); |
|
||||
} |
|
||||
|
|
||||
services.Add(new ServiceEntry((Service)value, property.Name, environments)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return services; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
internal class GroveledApplication : Application |
|
||||
{ |
|
||||
public GroveledApplication(ApplicationGlobals globals, string rootDirectory, IEnumerable<ServiceEntry> services) |
|
||||
{ |
|
||||
if (globals is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(globals)); |
|
||||
} |
|
||||
|
|
||||
if (rootDirectory is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(rootDirectory)); |
|
||||
} |
|
||||
|
|
||||
if (services is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(services)); |
|
||||
} |
|
||||
|
|
||||
Globals = globals; |
|
||||
RootDirectory = rootDirectory; |
|
||||
Services = services.ToArray(); |
|
||||
} |
|
||||
|
|
||||
public override ApplicationGlobals Globals { get; } |
|
||||
|
|
||||
public override string RootDirectory { get; } |
|
||||
|
|
||||
public override IReadOnlyList<ServiceEntry> Services { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,28 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class ApplicationBuilder |
||||
|
{ |
||||
|
public ApplicationBuilder(FileInfo source, string name) |
||||
|
{ |
||||
|
Source = source; |
||||
|
Name = name; |
||||
|
} |
||||
|
|
||||
|
public FileInfo Source { get; set; } |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public ContainerRegistry? Registry { get; set; } |
||||
|
|
||||
|
public List<ServiceBuilder> Services { get; } = new List<ServiceBuilder>(); |
||||
|
|
||||
|
public List<IngressBuilder> Ingress { get; } = new List<IngressBuilder>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,257 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Tye.ConfigModel; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public static class ApplicationFactory |
||||
|
{ |
||||
|
public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) |
||||
|
{ |
||||
|
if (source is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(source)); |
||||
|
} |
||||
|
|
||||
|
var config = ConfigFactory.FromFile(source); |
||||
|
ValidateConfigApplication(config); |
||||
|
|
||||
|
var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant()); |
||||
|
if (!string.IsNullOrEmpty(config.Registry)) |
||||
|
{ |
||||
|
builder.Registry = new ContainerRegistry(config.Registry); |
||||
|
} |
||||
|
|
||||
|
foreach (var configService in config.Services) |
||||
|
{ |
||||
|
ServiceBuilder service; |
||||
|
if (!string.IsNullOrEmpty(configService.Project)) |
||||
|
{ |
||||
|
var projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, configService.Project)); |
||||
|
var project = new ProjectServiceBuilder(configService.Name, projectFile); |
||||
|
service = project; |
||||
|
|
||||
|
project.Build = configService.Build ?? true; |
||||
|
project.Args = configService.Args; |
||||
|
project.Replicas = configService.Replicas ?? 1; |
||||
|
|
||||
|
await ProjectReader.ReadProjectDetailsAsync(output, project); |
||||
|
|
||||
|
// We don't apply more container defaults here because we might need
|
||||
|
// to prompty for the registry name.
|
||||
|
project.ContainerInfo = new ContainerInfo() |
||||
|
{ |
||||
|
UseMultiphaseDockerfile = false, |
||||
|
}; |
||||
|
} |
||||
|
else if (!string.IsNullOrEmpty(configService.Image)) |
||||
|
{ |
||||
|
var container = new ContainerServiceBuilder(configService.Name, configService.Image); |
||||
|
container.Args = configService.Args; |
||||
|
container.Replicas = configService.Replicas ?? 1; |
||||
|
service = container; |
||||
|
} |
||||
|
else if (!string.IsNullOrEmpty(configService.Executable)) |
||||
|
{ |
||||
|
var executable = new ExecutableServiceBuilder(configService.Name, configService.Executable); |
||||
|
executable.Args = configService.Args; |
||||
|
executable.WorkingDirectory = configService.WorkingDirectory; |
||||
|
executable.Replicas = configService.Replicas ?? 1; |
||||
|
service = executable; |
||||
|
} |
||||
|
else if (configService.External) |
||||
|
{ |
||||
|
var external = new ExternalServiceBuilder(configService.Name); |
||||
|
service = external; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new CommandException("Unable to determine service type."); |
||||
|
} |
||||
|
|
||||
|
builder.Services.Add(service); |
||||
|
|
||||
|
foreach (var configBinding in configService.Bindings) |
||||
|
{ |
||||
|
var binding = new BindingBuilder() |
||||
|
{ |
||||
|
Name = configBinding.Name, |
||||
|
AutoAssignPort = configBinding.AutoAssignPort, |
||||
|
ConnectionString = configBinding.ConnectionString, |
||||
|
Host = configBinding.Host, |
||||
|
InternalPort = configBinding.InternalPort, |
||||
|
Port = configBinding.Port, |
||||
|
Protocol = configBinding.Protocol, |
||||
|
}; |
||||
|
|
||||
|
if (binding.ConnectionString == null) |
||||
|
{ |
||||
|
binding.Protocol ??= "http"; |
||||
|
} |
||||
|
|
||||
|
service.Bindings.Add(binding); |
||||
|
} |
||||
|
|
||||
|
foreach (var configEnvVar in configService.Configuration) |
||||
|
{ |
||||
|
var envVar = new EnvironmentVariable(configEnvVar.Name, configEnvVar.Value); |
||||
|
if (service is ProjectServiceBuilder project) |
||||
|
{ |
||||
|
project.EnvironmentVariables.Add(envVar); |
||||
|
} |
||||
|
else if (service is ContainerServiceBuilder container) |
||||
|
{ |
||||
|
container.EnvironmentVariables.Add(envVar); |
||||
|
} |
||||
|
else if (service is ExecutableServiceBuilder executable) |
||||
|
{ |
||||
|
executable.EnvironmentVariables.Add(envVar); |
||||
|
} |
||||
|
else if (service is ExternalServiceBuilder) |
||||
|
{ |
||||
|
throw new CommandException("External services do not support environment variables."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new CommandException("Unable to determine service type."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach (var configVolume in configService.Volumes) |
||||
|
{ |
||||
|
var volume = new VolumeBuilder(configVolume.Source, configVolume.Target); |
||||
|
if (service is ProjectServiceBuilder project) |
||||
|
{ |
||||
|
project.Volumes.Add(volume); |
||||
|
} |
||||
|
else if (service is ContainerServiceBuilder container) |
||||
|
{ |
||||
|
container.Volumes.Add(volume); |
||||
|
} |
||||
|
else if (service is ExecutableServiceBuilder executable) |
||||
|
{ |
||||
|
throw new CommandException("Executable services do not support volumes."); |
||||
|
} |
||||
|
else if (service is ExternalServiceBuilder) |
||||
|
{ |
||||
|
throw new CommandException("External services do not support volumes."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new CommandException("Unable to determine service type."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach (var configIngress in config.Ingress) |
||||
|
{ |
||||
|
var ingress = new IngressBuilder(configIngress.Name); |
||||
|
ingress.Replicas = configIngress.Replicas ?? 1; |
||||
|
|
||||
|
builder.Ingress.Add(ingress); |
||||
|
|
||||
|
foreach (var configBinding in configIngress.Bindings) |
||||
|
{ |
||||
|
var binding = new IngressBindingBuilder() |
||||
|
{ |
||||
|
AutoAssignPort = configBinding.AutoAssignPort, |
||||
|
Name = configBinding.Name, |
||||
|
Port = configBinding.Port, |
||||
|
Protocol = configBinding.Protocol ?? "http", |
||||
|
}; |
||||
|
ingress.Bindings.Add(binding); |
||||
|
} |
||||
|
|
||||
|
foreach (var configRule in configIngress.Rules) |
||||
|
{ |
||||
|
var rule = new IngressRuleBuilder() |
||||
|
{ |
||||
|
Host = configRule.Host, |
||||
|
Path = configRule.Path, |
||||
|
Service = configRule.Service, |
||||
|
}; |
||||
|
ingress.Rules.Add(rule); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return builder; |
||||
|
} |
||||
|
|
||||
|
private static void ValidateConfigApplication(ConfigApplication config) |
||||
|
{ |
||||
|
var context = new ValidationContext(config); |
||||
|
var results = new List<ValidationResult>(); |
||||
|
if (!Validator.TryValidateObject(config, context, results, validateAllProperties: true)) |
||||
|
{ |
||||
|
throw new CommandException( |
||||
|
"Configuration validation failed." + Environment.NewLine + |
||||
|
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); |
||||
|
} |
||||
|
|
||||
|
foreach (var service in config.Services) |
||||
|
{ |
||||
|
context = new ValidationContext(service); |
||||
|
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true)) |
||||
|
{ |
||||
|
throw new CommandException( |
||||
|
$"Service '{service.Name}' validation failed." + Environment.NewLine + |
||||
|
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); |
||||
|
} |
||||
|
|
||||
|
foreach (var binding in service.Bindings) |
||||
|
{ |
||||
|
context = new ValidationContext(binding); |
||||
|
if (!Validator.TryValidateObject(binding, context, results, validateAllProperties: true)) |
||||
|
{ |
||||
|
throw new CommandException( |
||||
|
$"Binding '{binding.Name}' of service '{service.Name}' validation failed." + Environment.NewLine + |
||||
|
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach (var envVar in service.Configuration) |
||||
|
{ |
||||
|
context = new ValidationContext(service); |
||||
|
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true)) |
||||
|
{ |
||||
|
throw new CommandException( |
||||
|
$"Environment variable '{envVar.Name}' of service '{service.Name}' validation failed." + Environment.NewLine + |
||||
|
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach (var volume in service.Volumes) |
||||
|
{ |
||||
|
context = new ValidationContext(service); |
||||
|
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true)) |
||||
|
{ |
||||
|
throw new CommandException( |
||||
|
$"Volume '{volume.Source}' of service '{service.Name}' validation failed." + Environment.NewLine + |
||||
|
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach (var ingress in config.Ingress) |
||||
|
{ |
||||
|
// We don't currently recurse into ingress rules or ingress bindings right now.
|
||||
|
// There's nothing to validate there.
|
||||
|
context = new ValidationContext(ingress); |
||||
|
if (!Validator.TryValidateObject(ingress, context, results, validateAllProperties: true)) |
||||
|
{ |
||||
|
throw new CommandException( |
||||
|
$"Ingress '{ingress.Name}' validation failed." + Environment.NewLine + |
||||
|
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class BindingBuilder |
||||
|
{ |
||||
|
public string? Name { get; set; } |
||||
|
public string? ConnectionString { get; set; } |
||||
|
public bool AutoAssignPort { get; set; } |
||||
|
public int? Port { get; set; } |
||||
|
public int? InternalPort { get; set; } |
||||
|
public string? Host { get; set; } |
||||
|
public string? Protocol { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.IO; |
||||
|
using YamlDotNet.Serialization; |
||||
|
|
||||
|
namespace Microsoft.Tye.ConfigModel |
||||
|
{ |
||||
|
// General design note re: nullability - we use [Required] to validate for non-null
|
||||
|
// so we use = default! to reflect that. Code that creates a ConfigApplication should
|
||||
|
// validate it before deferencing the properties.
|
||||
|
public 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 List<ConfigIngress> Ingress { get; set; } = new List<ConfigIngress>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class ContainerServiceBuilder : ServiceBuilder |
||||
|
{ |
||||
|
public ContainerServiceBuilder(string name, string image) |
||||
|
: base(name) |
||||
|
{ |
||||
|
Image = image; |
||||
|
} |
||||
|
|
||||
|
public string Image { get; set; } |
||||
|
|
||||
|
public string? Args { get; set; } |
||||
|
|
||||
|
public int Replicas { get; set; } = 1; |
||||
|
|
||||
|
public List<EnvironmentVariable> EnvironmentVariables { get; } = new List<EnvironmentVariable>(); |
||||
|
|
||||
|
public List<VolumeBuilder> Volumes { get; } = new List<VolumeBuilder>(); |
||||
|
} |
||||
|
} |
||||
@ -1,163 +0,0 @@ |
|||||
// Licensed to the .NET Foundation under one or more agreements.
|
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
|
||||
// See the LICENSE file in the project root for more information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.IO; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Build.Construction; |
|
||||
|
|
||||
namespace Microsoft.Tye |
|
||||
{ |
|
||||
internal class CustomizationPipeline : Pipeline |
|
||||
{ |
|
||||
private readonly Dictionary<Type, List<MulticastDelegate>> callbacks = new Dictionary<Type, List<MulticastDelegate>>(); |
|
||||
|
|
||||
private readonly OutputContext output; |
|
||||
private readonly string rootDirectory; |
|
||||
private readonly string name; |
|
||||
private readonly SolutionFile? solution; |
|
||||
private readonly FileInfo? projectFile; |
|
||||
|
|
||||
public CustomizationPipeline(OutputContext output, string rootDirectory, string name, SolutionFile? solution, FileInfo? projectFile) |
|
||||
{ |
|
||||
if (output is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(output)); |
|
||||
} |
|
||||
|
|
||||
if (rootDirectory is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(rootDirectory)); |
|
||||
} |
|
||||
|
|
||||
if (name is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(name)); |
|
||||
} |
|
||||
|
|
||||
this.output = output; |
|
||||
this.rootDirectory = rootDirectory; |
|
||||
this.name = name; |
|
||||
this.solution = solution; |
|
||||
this.projectFile = projectFile; |
|
||||
} |
|
||||
|
|
||||
public override async Task<object?> ExecuteAsync() |
|
||||
{ |
|
||||
if (callbacks.Count == 0) |
|
||||
{ |
|
||||
return Task.FromResult<object?>(null); |
|
||||
} |
|
||||
|
|
||||
if (callbacks.Count > 1) |
|
||||
{ |
|
||||
throw new InvalidOperationException("More than one application type is not supported."); |
|
||||
} |
|
||||
|
|
||||
var kvp = callbacks.Single(); |
|
||||
|
|
||||
var type = kvp.Key; |
|
||||
var delegates = kvp.Value; |
|
||||
|
|
||||
output.WriteDebugLine($"Creating instance of application type '{type}'."); |
|
||||
var application = Activator.CreateInstance(type); |
|
||||
output.WriteDebugLine($"Done creating instance of application type '{type}'."); |
|
||||
|
|
||||
var wrapper = new ApplicationWrapper(application!, rootDirectory); |
|
||||
|
|
||||
wrapper.Globals.Name ??= name; |
|
||||
|
|
||||
foreach (var service in wrapper.Services) |
|
||||
{ |
|
||||
output.WriteDebugLine($"Found service '{service.FriendlyName} {{ Name: {service.Service.Name} }}'."); |
|
||||
|
|
||||
string? projectRelativeFilePath = null; |
|
||||
string? projectFilePath = null; |
|
||||
if (solution != null) |
|
||||
{ |
|
||||
var project = FindProjectInSolution(solution, service.FriendlyName); |
|
||||
if (project == null) |
|
||||
{ |
|
||||
output.WriteDebugLine($"Could not find project for service '{service.FriendlyName}'."); |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
output.WriteDebugLine($"Found project '{project.RelativePath}' for service '{service.FriendlyName}'."); |
|
||||
projectRelativeFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar); |
|
||||
projectFilePath = project.AbsolutePath.Replace('\\', Path.DirectorySeparatorChar); |
|
||||
} |
|
||||
else if (projectFile != null) |
|
||||
{ |
|
||||
var normalized = Names.NormalizeToFriendly(Path.GetFileNameWithoutExtension(projectFile.Name)); |
|
||||
if (!string.Equals(normalized, service.FriendlyName)) |
|
||||
{ |
|
||||
output.WriteDebugLine($"Skipping service '{service.FriendlyName}'."); |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
projectRelativeFilePath = projectFile.FullName; |
|
||||
projectFilePath = projectFile.FullName; |
|
||||
} |
|
||||
|
|
||||
if (projectFilePath != null) |
|
||||
{ |
|
||||
var project = new Project(projectRelativeFilePath!); |
|
||||
await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(projectFilePath), project); |
|
||||
|
|
||||
service.Service.Source = project; |
|
||||
|
|
||||
// Apply defaults to everything that has a project.
|
|
||||
var container = new ContainerInfo(); |
|
||||
service.Service.GeneratedAssets.Container = container; |
|
||||
DockerfileGenerator.ApplyContainerDefaults(wrapper, service, project, container); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
output.WriteDebugLine($"Running {delegates.Count} customization callbacks."); |
|
||||
for (var i = 0; i < delegates.Count; i++) |
|
||||
{ |
|
||||
delegates[i].DynamicInvoke(application); |
|
||||
} |
|
||||
output.WriteDebugLine($"Done running {delegates.Count} customization callbacks."); |
|
||||
|
|
||||
|
|
||||
return application; |
|
||||
} |
|
||||
|
|
||||
public override void Register<TApplication>(Action<TApplication> callback) |
|
||||
{ |
|
||||
if (callback is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(callback)); |
|
||||
} |
|
||||
|
|
||||
if (!callbacks.TryGetValue(typeof(TApplication), out var delegates)) |
|
||||
{ |
|
||||
delegates = new List<MulticastDelegate>(); |
|
||||
callbacks.Add(typeof(TApplication), delegates); |
|
||||
} |
|
||||
|
|
||||
delegates.Add(callback); |
|
||||
} |
|
||||
|
|
||||
private static ProjectInSolution? FindProjectInSolution(SolutionFile solution, string projectName) |
|
||||
{ |
|
||||
for (var i = 0; i < solution.ProjectsInOrder.Count; i++) |
|
||||
{ |
|
||||
var project = solution.ProjectsInOrder[i]; |
|
||||
var normalized = Names.NormalizeToFriendly(project.ProjectName); |
|
||||
|
|
||||
if (string.Equals(normalized, projectName, StringComparison.Ordinal) && |
|
||||
project.AbsolutePath.EndsWith(".csproj", StringComparison.Ordinal)) |
|
||||
{ |
|
||||
return project; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,31 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public class DockerImageOutput : ServiceOutput |
||||
|
{ |
||||
|
public DockerImageOutput(string imageName, string imageTag) |
||||
|
{ |
||||
|
if (imageName is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(imageName)); |
||||
|
} |
||||
|
|
||||
|
if (imageTag is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(imageTag)); |
||||
|
} |
||||
|
|
||||
|
ImageName = imageName; |
||||
|
ImageTag = imageTag; |
||||
|
} |
||||
|
|
||||
|
public string ImageName { get; } |
||||
|
|
||||
|
public string ImageTag { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class EnvironmentVariable |
||||
|
{ |
||||
|
public EnvironmentVariable(string name, string value) |
||||
|
{ |
||||
|
Name = name; |
||||
|
Value = value; |
||||
|
} |
||||
|
|
||||
|
public string Name { get; } |
||||
|
|
||||
|
public string Value { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class ExecutableServiceBuilder : ServiceBuilder |
||||
|
{ |
||||
|
public ExecutableServiceBuilder(string name, string executable) |
||||
|
: base(name) |
||||
|
{ |
||||
|
Executable = executable; |
||||
|
} |
||||
|
|
||||
|
public string Executable { get; set; } |
||||
|
|
||||
|
public string? WorkingDirectory { get; set; } |
||||
|
|
||||
|
public string? Args { get; set; } |
||||
|
|
||||
|
public int Replicas { get; set; } = 1; |
||||
|
|
||||
|
public List<EnvironmentVariable> EnvironmentVariables { get; } = new List<EnvironmentVariable>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class ExternalServiceBuilder : ServiceBuilder |
||||
|
{ |
||||
|
public ExternalServiceBuilder(string name) |
||||
|
: base(name) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class IngressBindingBuilder |
||||
|
{ |
||||
|
public string? Name { get; set; } |
||||
|
public bool AutoAssignPort { get; set; } |
||||
|
public int? Port { get; set; } |
||||
|
public string? Protocol { get; set; } // HTTP or HTTPS
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class IngressBuilder |
||||
|
{ |
||||
|
public IngressBuilder(string name) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public int Replicas { get; set; } = 1; |
||||
|
|
||||
|
public List<IngressBindingBuilder> Bindings { get; set; } = new List<IngressBindingBuilder>(); |
||||
|
|
||||
|
public List<IngressRuleBuilder> Rules { get; set; } = new List<IngressRuleBuilder>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class IngressRuleBuilder |
||||
|
{ |
||||
|
public string? Path { get; set; } |
||||
|
public string? Host { get; set; } |
||||
|
public string? Service { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class ProjectServiceBuilder : ServiceBuilder |
||||
|
{ |
||||
|
public ProjectServiceBuilder(string name, FileInfo projectFile) |
||||
|
: base(name) |
||||
|
{ |
||||
|
ProjectFile = projectFile; |
||||
|
} |
||||
|
|
||||
|
public FileInfo ProjectFile { get; } |
||||
|
|
||||
|
public int Replicas { get; set; } = 1; |
||||
|
|
||||
|
public bool Build { get; set; } |
||||
|
|
||||
|
public string? Args { get; set; } |
||||
|
|
||||
|
public FrameworkCollection Frameworks { get; } = new FrameworkCollection(); |
||||
|
|
||||
|
// This is always set on the ApplicationFactory codepath.
|
||||
|
public string TargetFramework { get; set; } = default!; |
||||
|
|
||||
|
// This is always set on the ApplicationFactory codepath.
|
||||
|
public string Version { get; set; } = default!; |
||||
|
|
||||
|
// Data used for building containers
|
||||
|
public ContainerInfo? ContainerInfo { get; set; } |
||||
|
|
||||
|
public List<EnvironmentVariable> EnvironmentVariables { get; } = new List<EnvironmentVariable>(); |
||||
|
|
||||
|
// Used when running in a container locally.
|
||||
|
public List<VolumeBuilder> Volumes { get; } = new List<VolumeBuilder>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public abstract class ServiceBuilder |
||||
|
{ |
||||
|
public ServiceBuilder(string name) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
|
||||
|
public string Name { get; } |
||||
|
|
||||
|
public List<BindingBuilder> Bindings { get; } = new List<BindingBuilder>(); |
||||
|
|
||||
|
// TODO: this is temporary while refactoring
|
||||
|
public List<ServiceOutput> Outputs { get; } = new List<ServiceOutput>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public abstract class ServiceOutput |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class VolumeBuilder |
||||
|
{ |
||||
|
public VolumeBuilder(string source, string target) |
||||
|
{ |
||||
|
Source = source; |
||||
|
Target = target; |
||||
|
} |
||||
|
|
||||
|
public string Source { get; set; } |
||||
|
|
||||
|
public string Target { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -1,30 +0,0 @@ |
|||||
// Licensed to the .NET Foundation under one or more agreements.
|
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
|
||||
// See the LICENSE file in the project root for more information.
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using Microsoft.Tye.ConfigModel; |
|
||||
|
|
||||
namespace Microsoft.Tye |
|
||||
{ |
|
||||
internal class TemporaryApplicationAdapter : Tye.Application |
|
||||
{ |
|
||||
private readonly ConfigApplication application; |
|
||||
|
|
||||
public TemporaryApplicationAdapter( |
|
||||
ConfigApplication application, |
|
||||
ApplicationGlobals globals, |
|
||||
IReadOnlyList<ServiceEntry> services) |
|
||||
{ |
|
||||
this.application = application; |
|
||||
Globals = globals; |
|
||||
Services = services; |
|
||||
} |
|
||||
|
|
||||
public override ApplicationGlobals Globals { get; } |
|
||||
|
|
||||
public override string RootDirectory => application.Source.DirectoryName; |
|
||||
|
|
||||
public override IReadOnlyList<ServiceEntry> Services { get; } |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue