diff --git a/src/Microsoft.Tye.Core/Application.cs b/src/Microsoft.Tye.Core/Application.cs deleted file mode 100644 index 28ad5e25..00000000 --- a/src/Microsoft.Tye.Core/Application.cs +++ /dev/null @@ -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 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? 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(); - } - - public IReadOnlyList Environments { get; } - - public string FriendlyName { get; } - - public Service Service { get; } - - public List Outputs { get; } = new List(); - - 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? 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 Services - { - get - { - if (services == null) - { - services = new List(); - 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(); - var environmentAttributes = property.GetCustomAttributes(); - 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 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 Services { get; } - } -} diff --git a/src/Microsoft.Tye.Core/ApplicationBuilder.cs b/src/Microsoft.Tye.Core/ApplicationBuilder.cs new file mode 100644 index 00000000..7b794c46 --- /dev/null +++ b/src/Microsoft.Tye.Core/ApplicationBuilder.cs @@ -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 Services { get; } = new List(); + + public List Ingress { get; } = new List(); + } +} diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs new file mode 100644 index 00000000..b5ce7a71 --- /dev/null +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -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 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(); + 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))); + } + } + } + } +} diff --git a/src/Microsoft.Tye.Core/ApplicationYamlWriter.cs b/src/Microsoft.Tye.Core/ApplicationYamlWriter.cs index e997f199..d9f9b458 100644 --- a/src/Microsoft.Tye.Core/ApplicationYamlWriter.cs +++ b/src/Microsoft.Tye.Core/ApplicationYamlWriter.cs @@ -11,7 +11,7 @@ namespace Microsoft.Tye { public sealed class ApplicationYamlWriter { - public static Task WriteAsync(OutputContext output, StreamWriter writer, Application application) + public static Task WriteAsync(OutputContext output, StreamWriter writer, ApplicationBuilder application) { var yaml = application.Services.SelectMany(s => s.Outputs.OfType()).ToArray(); if (yaml.Length == 0) diff --git a/src/Microsoft.Tye.Core/BindingBuilder.cs b/src/Microsoft.Tye.Core/BindingBuilder.cs new file mode 100644 index 00000000..538f4e05 --- /dev/null +++ b/src/Microsoft.Tye.Core/BindingBuilder.cs @@ -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; } + } +} diff --git a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs index 1588ca57..de208a99 100644 --- a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs +++ b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs @@ -12,7 +12,7 @@ namespace Microsoft.Tye public string Environment { get; set; } = "production"; - public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutProject(output, service, out var project)) { @@ -24,22 +24,17 @@ namespace Microsoft.Tye return; } - if (SkipForEnvironment(output, service, Environment)) - { - return; - } - if (!await DockerDetector.Instance.IsDockerInstalled.Value) { - throw new CommandException($"Cannot generate a docker image for '{service.Service.Name}' because docker is not installed."); + throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not installed."); } if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value) { - throw new CommandException($"Cannot generate a docker image for '{service.Service.Name}' because docker is not running."); + throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not running."); } - await DockerContainerBuilder.BuildContainerImageAsync(output, application, service, project, container); + await DockerContainerBuilder.BuildContainerImageAsync(output, application, project, container); } } } diff --git a/src/Microsoft.Tye.Core/CombineStep.cs b/src/Microsoft.Tye.Core/CombineStep.cs index 914a923b..800b7793 100644 --- a/src/Microsoft.Tye.Core/CombineStep.cs +++ b/src/Microsoft.Tye.Core/CombineStep.cs @@ -2,6 +2,7 @@ // 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.Globalization; using System.Threading.Tasks; namespace Microsoft.Tye @@ -12,23 +13,33 @@ namespace Microsoft.Tye public string Environment { get; set; } = "production"; - public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { // No need to do this computation for a non-project since we're not deploying it. - if (!(service.Service.Source is Project)) + if (!(service is ProjectServiceBuilder project)) { return Task.CompletedTask; } // Compute ASPNETCORE_URLS based on the bindings exposed by *this* project. - foreach (var binding in service.Service.Bindings) + foreach (var binding in service.Bindings) { - if (binding.Protocol == "http" || binding.Protocol == null) + if (binding.Protocol == null && binding.ConnectionString == null) + { + binding.Protocol = "http"; + } + + if (binding.AutoAssignPort && binding.Port == null && binding.Protocol == "http") + { + binding.Port = 80; + } + + if (binding.Protocol == "http") { var port = binding.Port ?? 80; var urls = $"http://*{(port == 80 ? "" : (":" + port.ToString()))}"; - service.Service.Environment.Add("ASPNETCORE_URLS", urls); - service.Service.Environment.Add("PORT", port); + project.EnvironmentVariables.Add(new EnvironmentVariable("ASPNETCORE_URLS", urls)); + project.EnvironmentVariables.Add(new EnvironmentVariable("PORT", port.ToString(CultureInfo.InvariantCulture))); break; } } @@ -47,17 +58,21 @@ namespace Microsoft.Tye continue; } - foreach (var binding in other.Service.Bindings) + foreach (var binding in other.Bindings) { // The other thing is a project, and will be deployed along with this // service. - var configName = binding.Name == other.Service.Name ? other.Service.Name.ToUpperInvariant() : $"{other.Service.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}"; - if (other.Service.Source is Project) + var configName = + (binding.Name is null || binding.Name == other.Name) ? + other.Name.ToUpperInvariant() : + $"{other.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}"; + if (other is ProjectServiceBuilder) { if (!string.IsNullOrEmpty(binding.ConnectionString)) { // Special case for connection strings bindings.Bindings.Add(new EnvironmentVariableInputBinding($"CONNECTIONSTRING__{configName}", binding.ConnectionString)); + continue; } if (binding.Protocol == "https") @@ -67,6 +82,16 @@ namespace Microsoft.Tye continue; } + if (binding.Protocol == null) + { + binding.Protocol = "http"; + } + + if (binding.AutoAssignPort && binding.Port == null && binding.Protocol == "http") + { + binding.Port = 80; + } + if (!string.IsNullOrEmpty(binding.Protocol)) { bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PROTOCOL", binding.Protocol)); @@ -77,13 +102,13 @@ namespace Microsoft.Tye bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PORT", binding.Port.Value.ToString())); } - bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Service.Name)); + bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Name)); } else { // The other service is not a project, so we'll use secrets. bindings.Bindings.Add(new SecretInputBinding( - name: $"binding-{Environment}-{other.Service.Name}-{binding.Name}-secret", + name: $"binding-{Environment}-{other.Name}-{binding.Name ?? other.Name}-secret", filename: $"CONNECTIONSTRING__{configName}", other, binding)); diff --git a/src/Microsoft.Tye.Core/ComputedBindings.cs b/src/Microsoft.Tye.Core/ComputedBindings.cs index 74b07cb7..e955efed 100644 --- a/src/Microsoft.Tye.Core/ComputedBindings.cs +++ b/src/Microsoft.Tye.Core/ComputedBindings.cs @@ -29,7 +29,7 @@ namespace Microsoft.Tye public sealed class SecretInputBinding : InputBinding { - public SecretInputBinding(string name, string filename, ServiceEntry service, ServiceBinding binding) + public SecretInputBinding(string name, string filename, ServiceBuilder service, BindingBuilder binding) { Name = name; Filename = filename; @@ -45,7 +45,7 @@ namespace Microsoft.Tye public string Filename { get; } // Used for informational purposes - public ServiceEntry Service { get; } - public ServiceBinding Binding { get; } + public ServiceBuilder Service { get; } + public BindingBuilder Binding { get; } } } diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs new file mode 100644 index 00000000..5e395154 --- /dev/null +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs @@ -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 Services { get; set; } = new List(); + + public List Ingress { get; set; } = new List(); + } +} diff --git a/src/tye/ConfigModel/ConfigConfigurationSource.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigConfigurationSource.cs similarity index 91% rename from src/tye/ConfigModel/ConfigConfigurationSource.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigConfigurationSource.cs index 6d14c654..888e4f03 100644 --- a/src/tye/ConfigModel/ConfigConfigurationSource.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigConfigurationSource.cs @@ -10,8 +10,8 @@ namespace Microsoft.Tye.ConfigModel { [Required] public string Name { get; set; } = default!; + [Required] public string Value { get; set; } = default!; - public string? Source { get; set; } } } diff --git a/src/tye/ConfigModel/ConfigFactory.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs similarity index 91% rename from src/tye/ConfigModel/ConfigFactory.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs index 3d24405a..43c1c7bf 100644 --- a/src/tye/ConfigModel/ConfigFactory.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs @@ -152,17 +152,8 @@ namespace Microsoft.Tye.ConfigModel } } - 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() - }); - } - } + // Don't apply environment variables here. We don't want to carry forward settings from + // development into production. } } } diff --git a/src/tye/ConfigModel/ConfigIngress.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs similarity index 75% rename from src/tye/ConfigModel/ConfigIngress.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs index ac3be59d..23429427 100644 --- a/src/tye/ConfigModel/ConfigIngress.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs @@ -2,17 +2,17 @@ // 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.Text; +using System.ComponentModel.DataAnnotations; namespace Microsoft.Tye.ConfigModel { public class ConfigIngress { + [Required] public string Name { get; set; } = default!; public int? Replicas { get; set; } public List Rules { get; set; } = new List(); - public List Bindings { get; set; } = new List(); + public List Bindings { get; set; } = new List(); } } diff --git a/src/tye/ConfigModel/ConfigIngressServiceBinding.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs similarity index 80% rename from src/tye/ConfigModel/ConfigIngressServiceBinding.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs index a44f1a0a..81f5fb4a 100644 --- a/src/tye/ConfigModel/ConfigIngressServiceBinding.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs @@ -2,13 +2,9 @@ // 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.Text; - namespace Microsoft.Tye.ConfigModel { - public class ConfigIngressServiceBinding + public class ConfigIngressBinding { public string? Name { get; set; } public bool AutoAssignPort { get; set; } diff --git a/src/tye/ConfigModel/ConfigIngressRule.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressRule.cs similarity index 86% rename from src/tye/ConfigModel/ConfigIngressRule.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigIngressRule.cs index a0228180..4a94f462 100644 --- a/src/tye/ConfigModel/ConfigIngressRule.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressRule.cs @@ -2,10 +2,6 @@ // 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.Text; - namespace Microsoft.Tye.ConfigModel { public class ConfigIngressRule diff --git a/src/tye/ConfigModel/ConfigService.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs similarity index 100% rename from src/tye/ConfigModel/ConfigService.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs diff --git a/src/tye/ConfigModel/ConfigServiceBinding.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigServiceBinding.cs similarity index 100% rename from src/tye/ConfigModel/ConfigServiceBinding.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigServiceBinding.cs diff --git a/src/tye/ConfigModel/ConfigVolume.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigVolume.cs similarity index 59% rename from src/tye/ConfigModel/ConfigVolume.cs rename to src/Microsoft.Tye.Core/ConfigModel/ConfigVolume.cs index b0d3964d..964e71d6 100644 --- a/src/tye/ConfigModel/ConfigVolume.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigVolume.cs @@ -2,15 +2,16 @@ // 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.Text; +using System.ComponentModel.DataAnnotations; namespace Microsoft.Tye.ConfigModel { public class ConfigVolume { - public string? Source { get; set; } - public string? Target { get; set; } + [Required] + public string Source { get; set; } = default!; + + [Required] + public string Target { get; set; } = default!; } } diff --git a/src/Microsoft.Tye.Core/ContainerServiceBuilder.cs b/src/Microsoft.Tye.Core/ContainerServiceBuilder.cs new file mode 100644 index 00000000..187677e3 --- /dev/null +++ b/src/Microsoft.Tye.Core/ContainerServiceBuilder.cs @@ -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 EnvironmentVariables { get; } = new List(); + + public List Volumes { get; } = new List(); + } +} diff --git a/src/Microsoft.Tye.Core/CustomizationPipeline.cs b/src/Microsoft.Tye.Core/CustomizationPipeline.cs deleted file mode 100644 index c237f5e6..00000000 --- a/src/Microsoft.Tye.Core/CustomizationPipeline.cs +++ /dev/null @@ -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> callbacks = new Dictionary>(); - - 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 ExecuteAsync() - { - if (callbacks.Count == 0) - { - return Task.FromResult(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(Action callback) - { - if (callback is null) - { - throw new ArgumentNullException(nameof(callback)); - } - - if (!callbacks.TryGetValue(typeof(TApplication), out var delegates)) - { - delegates = new List(); - 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; - } - } -} diff --git a/src/Microsoft.Tye.Core/DeployServiceYamlStep.cs b/src/Microsoft.Tye.Core/DeployServiceYamlStep.cs index d71477f6..5f65612c 100644 --- a/src/Microsoft.Tye.Core/DeployServiceYamlStep.cs +++ b/src/Microsoft.Tye.Core/DeployServiceYamlStep.cs @@ -17,23 +17,23 @@ namespace Microsoft.Tye public string Environment { get; set; } = "production"; - public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { var yaml = service.Outputs.OfType().ToArray(); if (yaml.Length == 0) { - output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping."); + output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping."); return; } if (!await KubectlDetector.Instance.IsKubectlInstalled.Value) { - throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not installed."); + throw new CommandException($"Cannot apply manifests for '{service.Name}' because kubectl is not installed."); } if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value) { - throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not connected to a cluster."); + throw new CommandException($"Cannot apply manifests for '{service.Name}' because kubectl is not connected to a cluster."); } using var tempFile = TempFile.Create(); @@ -67,7 +67,7 @@ namespace Microsoft.Tye throw new CommandException("'kubectl apply' failed."); } - output.WriteInfoLine($"Deployed service '{service.FriendlyName}'."); + output.WriteInfoLine($"Deployed service '{service.Name}'."); } } } diff --git a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs index 8077f535..e7260e42 100644 --- a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs +++ b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs @@ -12,7 +12,7 @@ namespace Microsoft.Tye { internal static class DockerContainerBuilder { - public static async Task BuildContainerImageAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container) + public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container) { if (output is null) { @@ -24,11 +24,6 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(application)); } - if (service is null) - { - throw new ArgumentNullException(nameof(service)); - } - if (project is null) { throw new ArgumentNullException(nameof(project)); @@ -41,14 +36,14 @@ namespace Microsoft.Tye using var tempFile = TempFile.Create(); - var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), Path.GetDirectoryName(project.RelativeFilePath)!, "Dockerfile"); + var dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName, "Dockerfile"); if (File.Exists(dockerFilePath)) { output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'."); } else { - await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, tempFile.FilePath); + await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath); dockerFilePath = tempFile.FilePath; } @@ -61,7 +56,7 @@ namespace Microsoft.Tye } else { - var publishOutput = service.Outputs.OfType().FirstOrDefault(); + var publishOutput = project.Outputs.OfType().FirstOrDefault(); if (publishOutput is null) { throw new InvalidOperationException("We should have published the project for a single-phase Dockerfile."); @@ -76,7 +71,7 @@ namespace Microsoft.Tye var exitCode = await Process.ExecuteAsync( $"docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"", - application.GetProjectDirectory(project), + project.ProjectFile.DirectoryName, stdOut: capture.StdOut, stdErr: capture.StdErr); @@ -87,7 +82,7 @@ namespace Microsoft.Tye } output.WriteInfoLine($"Created Docker Image: '{container.ImageName}:{container.ImageTag}'"); - service.Outputs.Add(new DockerImageOutput(container.ImageName!, container.ImageTag!)); + project.Outputs.Add(new DockerImageOutput(container.ImageName!, container.ImageTag!)); } } } diff --git a/src/Microsoft.Tye.Core/DockerImageOutput.cs b/src/Microsoft.Tye.Core/DockerImageOutput.cs new file mode 100644 index 00000000..4c0ff5da --- /dev/null +++ b/src/Microsoft.Tye.Core/DockerImageOutput.cs @@ -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; } + } +} diff --git a/src/Microsoft.Tye.Core/DockerfileGenerator.cs b/src/Microsoft.Tye.Core/DockerfileGenerator.cs index 88af25a3..17003a02 100644 --- a/src/Microsoft.Tye.Core/DockerfileGenerator.cs +++ b/src/Microsoft.Tye.Core/DockerfileGenerator.cs @@ -12,7 +12,7 @@ namespace Microsoft.Tye { public static class DockerfileGenerator { - public static async Task WriteDockerfileAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, string filePath) + public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, string filePath) { if (output is null) { @@ -24,11 +24,6 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(application)); } - if (service is null) - { - throw new ArgumentNullException(nameof(service)); - } - if (project is null) { throw new ArgumentNullException(nameof(project)); @@ -47,7 +42,7 @@ namespace Microsoft.Tye using var stream = File.OpenWrite(filePath); using var writer = new StreamWriter(stream, encoding: Encoding.UTF8, bufferSize: -1, leaveOpen: true); - var entryPoint = Path.GetFileNameWithoutExtension(project.RelativeFilePath); + var entryPoint = Path.GetFileNameWithoutExtension(project.ProjectFile.Name); output.WriteDebugLine($"Writing Dockerfile to '{filePath}'."); if (container.UseMultiphaseDockerfile ?? true) { @@ -80,18 +75,13 @@ namespace Microsoft.Tye await writer.WriteLineAsync($"ENTRYPOINT [\"dotnet\", \"{applicationEntryPoint}.dll\"]"); } - public static void ApplyContainerDefaults(Application application, ServiceEntry service, Project project, ContainerInfo container) + public static void ApplyContainerDefaults(ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container) { if (application is null) { throw new ArgumentNullException(nameof(application)); } - if (service is null) - { - throw new ArgumentNullException(nameof(service)); - } - if (project is null) { throw new ArgumentNullException(nameof(project)); @@ -131,16 +121,16 @@ namespace Microsoft.Tye container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk"; container.BuildImageTag ??= "3.1"; - if (container.ImageName == null && application.Globals.Registry?.Hostname == null) + if (container.ImageName == null && application.Registry?.Hostname == null) { - container.ImageName ??= service.Service.Name.ToLowerInvariant(); + container.ImageName ??= project.Name.ToLowerInvariant(); } - else if (container.ImageName == null && application.Globals.Registry?.Hostname != null) + else if (container.ImageName == null && application.Registry?.Hostname != null) { - container.ImageName ??= $"{application.Globals.Registry?.Hostname}/{service.Service.Name.ToLowerInvariant()}"; + container.ImageName ??= $"{application.Registry?.Hostname}/{project.Name.ToLowerInvariant()}"; } - container.ImageTag ??= project.Version.Replace("+", "-"); + container.ImageTag ??= project.Version?.Replace("+", "-") ?? "latest"; } } } diff --git a/src/Microsoft.Tye.Core/EnvironmentVariable.cs b/src/Microsoft.Tye.Core/EnvironmentVariable.cs new file mode 100644 index 00000000..3f120ec5 --- /dev/null +++ b/src/Microsoft.Tye.Core/EnvironmentVariable.cs @@ -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; } + } +} diff --git a/src/Microsoft.Tye.Core/ExecutableServiceBuilder.cs b/src/Microsoft.Tye.Core/ExecutableServiceBuilder.cs new file mode 100644 index 00000000..70e1eef5 --- /dev/null +++ b/src/Microsoft.Tye.Core/ExecutableServiceBuilder.cs @@ -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 EnvironmentVariables { get; } = new List(); + } +} diff --git a/src/Microsoft.Tye.Core/ExternalServiceBuilder.cs b/src/Microsoft.Tye.Core/ExternalServiceBuilder.cs new file mode 100644 index 00000000..158c4662 --- /dev/null +++ b/src/Microsoft.Tye.Core/ExternalServiceBuilder.cs @@ -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) + { + } + } +} diff --git a/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs b/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs index 02b59adf..f871820a 100644 --- a/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs +++ b/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs @@ -13,7 +13,7 @@ namespace Microsoft.Tye public bool Force { get; set; } - public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutProject(output, service, out var project)) { @@ -32,7 +32,7 @@ namespace Microsoft.Tye container.UseMultiphaseDockerfile ??= true; - var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), "Dockerfile"); + var dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName, "Dockerfile"); if (File.Exists(dockerFilePath) && !Force) { throw new CommandException("'Dockerfile' already exists for project. use '--force' to overwrite."); @@ -40,7 +40,7 @@ namespace Microsoft.Tye File.Delete(dockerFilePath); - await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, dockerFilePath); + await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, dockerFilePath); output.WriteInfoLine($"Generated Dockerfile at '{dockerFilePath}'."); } } diff --git a/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs b/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs index be64e592..500fd7d3 100644 --- a/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs +++ b/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs @@ -13,7 +13,7 @@ namespace Microsoft.Tye public bool Force { get; set; } - public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutProject(output, service, out var project)) { @@ -25,7 +25,7 @@ namespace Microsoft.Tye return; } - var chartDirectory = Path.Combine(application.GetProjectDirectory(project), "charts"); + var chartDirectory = Path.Combine(project.ProjectFile.DirectoryName, "charts"); if (Directory.Exists(chartDirectory) && !Force) { throw new CommandException("'charts' directory already exists for project. use '--force' to overwrite."); @@ -39,7 +39,6 @@ namespace Microsoft.Tye await HelmChartGenerator.GenerateAsync( output, application, - service, project, container, chart, diff --git a/src/Microsoft.Tye.Core/GenerateKubernetesManifestStep.cs b/src/Microsoft.Tye.Core/GenerateKubernetesManifestStep.cs index 9ae425ff..559b2e3a 100644 --- a/src/Microsoft.Tye.Core/GenerateKubernetesManifestStep.cs +++ b/src/Microsoft.Tye.Core/GenerateKubernetesManifestStep.cs @@ -13,23 +13,23 @@ namespace Microsoft.Tye public string Environment { get; set; } = "production"; - public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutContainerOutput(output, service)) { return Task.CompletedTask; } - if (SkipForEnvironment(output, service, Environment)) + if (SkipWithoutProject(output, service, out var project)) { return Task.CompletedTask; } - service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, service)); + service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, project)); - if (service.Service.Bindings.Count > 0) + if (service.Bindings.Count > 0) { - service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, service)); + service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, project)); } return Task.CompletedTask; diff --git a/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs b/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs index 56f220d6..4bed6499 100644 --- a/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs +++ b/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs @@ -13,19 +13,19 @@ namespace Microsoft.Tye public string Environment { get; set; } = "production"; - public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutContainerOutput(output, service)) { return Task.CompletedTask; } - if (SkipForEnvironment(output, service, Environment)) + if (SkipWithoutProject(output, service, out var project)) { return Task.CompletedTask; } - var component = OamComponentGenerator.CreateOamComponent(output, application, service); + var component = OamComponentGenerator.CreateOamComponent(output, application, project); service.Outputs.Add(component); return Task.CompletedTask; } diff --git a/src/Microsoft.Tye.Core/HelmChartBuilder.cs b/src/Microsoft.Tye.Core/HelmChartBuilder.cs index c173f478..b39c76cf 100644 --- a/src/Microsoft.Tye.Core/HelmChartBuilder.cs +++ b/src/Microsoft.Tye.Core/HelmChartBuilder.cs @@ -11,7 +11,7 @@ namespace Microsoft.Tye { internal static class HelmChartBuilder { - public static async Task BuildHelmChartAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart) + public static async Task BuildHelmChartAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart) { if (output is null) { @@ -23,11 +23,6 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(application)); } - if (service is null) - { - throw new ArgumentNullException(nameof(service)); - } - if (project is null) { throw new ArgumentNullException(nameof(project)); @@ -43,11 +38,11 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(chart)); } - var projectDirectory = Path.Combine(application.RootDirectory, Path.GetDirectoryName(project.RelativeFilePath)!); + var projectDirectory = project.ProjectFile.DirectoryName; var outputDirectoryPath = Path.Combine(projectDirectory, "bin"); using var tempDirectory = TempDirectory.Create(); - HelmChartGenerator.ApplyHelmChartDefaults(application, service, container, chart); + HelmChartGenerator.ApplyHelmChartDefaults(application, project, container, chart); var chartRoot = Path.Combine(projectDirectory, "charts"); var chartPath = Path.Combine(chartRoot, chart.ChartName); @@ -62,7 +57,7 @@ namespace Microsoft.Tye chartRoot = tempDirectory.DirectoryPath; chartPath = Path.Combine(chartRoot, chart.ChartName); output.WriteDebugLine($"Generating chart in '{chartPath}'."); - await HelmChartGenerator.GenerateAsync(output, application, service, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath)); + await HelmChartGenerator.GenerateAsync(output, application, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath)); } output.WriteDebugLine("Running 'helm package'."); diff --git a/src/Microsoft.Tye.Core/HelmChartGenerator.cs b/src/Microsoft.Tye.Core/HelmChartGenerator.cs index 2a3cad1d..4d9d6f14 100644 --- a/src/Microsoft.Tye.Core/HelmChartGenerator.cs +++ b/src/Microsoft.Tye.Core/HelmChartGenerator.cs @@ -10,7 +10,7 @@ namespace Microsoft.Tye { internal static class HelmChartGenerator { - public static async Task GenerateAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory) + public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory) { if (output is null) { @@ -22,11 +22,6 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(application)); } - if (service is null) - { - throw new ArgumentNullException(nameof(service)); - } - if (project is null) { throw new ArgumentNullException(nameof(project)); @@ -47,7 +42,7 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(outputDirectory)); } - ApplyHelmChartDefaults(application, service, container, chart); + ApplyHelmChartDefaults(application, project, container, chart); // The directory with the charts needs to be the same as the chart name var chartDirectoryPath = Path.Combine(outputDirectory.FullName, chart.ChartName); @@ -87,7 +82,7 @@ namespace Microsoft.Tye }); } - public static void ApplyHelmChartDefaults(Application application, ServiceEntry service, ContainerInfo container, HelmChartStep chart) + public static void ApplyHelmChartDefaults(ApplicationBuilder application, ServiceBuilder service, ContainerInfo container, HelmChartStep chart) { if (application is null) { @@ -109,7 +104,7 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(chart)); } - chart.ChartName ??= service.Service.Name.ToLowerInvariant(); + chart.ChartName ??= service.Name.ToLowerInvariant(); } } } diff --git a/src/Microsoft.Tye.Core/IngressBindingBuilder.cs b/src/Microsoft.Tye.Core/IngressBindingBuilder.cs new file mode 100644 index 00000000..d5082087 --- /dev/null +++ b/src/Microsoft.Tye.Core/IngressBindingBuilder.cs @@ -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 + } +} diff --git a/src/Microsoft.Tye.Core/IngressBuilder.cs b/src/Microsoft.Tye.Core/IngressBuilder.cs new file mode 100644 index 00000000..456fdc43 --- /dev/null +++ b/src/Microsoft.Tye.Core/IngressBuilder.cs @@ -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 Bindings { get; set; } = new List(); + + public List Rules { get; set; } = new List(); + } +} diff --git a/src/Microsoft.Tye.Core/IngressRuleBuilder.cs b/src/Microsoft.Tye.Core/IngressRuleBuilder.cs new file mode 100644 index 00000000..3151673d --- /dev/null +++ b/src/Microsoft.Tye.Core/IngressRuleBuilder.cs @@ -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; } + } +} diff --git a/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs b/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs index a1a97a88..6870ccfd 100644 --- a/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs +++ b/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs @@ -11,7 +11,7 @@ namespace Microsoft.Tye { internal static class KubernetesManifestGenerator { - public static ServiceOutput CreateService(OutputContext output, Application application, ServiceEntry service) + public static ServiceOutput CreateService(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (output is null) { @@ -35,23 +35,20 @@ namespace Microsoft.Tye var metadata = new YamlMappingNode(); root.Add("metadata", metadata); - metadata.Add("name", service.Service.Name); + metadata.Add("name", service.Name); var labels = new YamlMappingNode(); metadata.Add("labels", labels); - labels.Add("app.kubernetes.io/name", service.Service.Name); + labels.Add("app.kubernetes.io/name", service.Name); - if (application.Globals.Name is object) - { - labels.Add("app.kubernetes.io/part-of", application.Globals.Name); - } + labels.Add("app.kubernetes.io/part-of", application.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); var selector = new YamlMappingNode(); spec.Add("selector", selector); - selector.Add("app.kubernetes.io/name", service.Service.Name); + selector.Add("app.kubernetes.io/name", service.Name); spec.Add("type", "ClusterIP"); @@ -59,7 +56,7 @@ namespace Microsoft.Tye spec.Add("ports", ports); // We figure out the port based on bindings - foreach (var binding in service.Service.Bindings) + foreach (var binding in service.Bindings) { if (binding.Protocol == "https") { @@ -80,10 +77,10 @@ namespace Microsoft.Tye } } - return new KubernetesServiceOutput(service.Service.Name, new YamlDocument(root)); + return new KubernetesServiceOutput(service.Name, new YamlDocument(root)); } - public static ServiceOutput CreateDeployment(OutputContext output, Application application, ServiceEntry service) + public static ServiceOutput CreateDeployment(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project) { if (output is null) { @@ -95,12 +92,12 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(application)); } - if (service is null) + if (project is null) { - throw new ArgumentNullException(nameof(service)); + throw new ArgumentNullException(nameof(project)); } - var bindings = service.Outputs.OfType().FirstOrDefault(); + var bindings = project.Outputs.OfType().FirstOrDefault(); var root = new YamlMappingNode(); @@ -109,27 +106,24 @@ namespace Microsoft.Tye var metadata = new YamlMappingNode(); root.Add("metadata", metadata); - metadata.Add("name", service.Service.Name); + metadata.Add("name", project.Name); var labels = new YamlMappingNode(); metadata.Add("labels", labels); - labels.Add("app.kubernetes.io/name", service.Service.Name); - if (application.Globals.Name is object) - { - labels.Add("app.kubernetes.io/part-of", application.Globals.Name); - } + labels.Add("app.kubernetes.io/name", project.Name); + labels.Add("app.kubernetes.io/part-of", application.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); - spec.Add("replicas", service.Service.Replicas.ToString()); + spec.Add("replicas", project.Replicas.ToString()); var selector = new YamlMappingNode(); spec.Add("selector", selector); var matchLabels = new YamlMappingNode(); selector.Add("matchLabels", matchLabels); - matchLabels.Add("app.kubernetes.io/name", service.Service.Name); + matchLabels.Add("app.kubernetes.io/name", project.Name); var template = new YamlMappingNode(); spec.Add("template", template); @@ -139,11 +133,8 @@ namespace Microsoft.Tye labels = new YamlMappingNode(); metadata.Add("labels", labels); - labels.Add("app.kubernetes.io/name", service.Service.Name); - if (application.Globals.Name is object) - { - labels.Add("app.kubernetes.io/part-of", application.Globals.Name); - } + labels.Add("app.kubernetes.io/name", project.Name); + labels.Add("app.kubernetes.io/part-of", application.Name); spec = new YamlMappingNode(); template.Add("spec", spec); @@ -151,19 +142,19 @@ namespace Microsoft.Tye var containers = new YamlSequenceNode(); spec.Add("containers", containers); - var images = service.Outputs.OfType(); + var images = project.Outputs.OfType(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); - container.Add("name", service.Service.Name); // NOTE: to really support multiple images we'd need to generate unique names. + container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning - if (service.Service.Environment.Count > 0 || + if (project.EnvironmentVariables.Count > 0 || // We generate ASPNETCORE_URLS if there are bindings for http - service.Service.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || + project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || // We generate environment variables for other services if there dependencies (bindings is object && bindings.Bindings.OfType().Any())) @@ -171,12 +162,12 @@ namespace Microsoft.Tye var env = new YamlSequenceNode(); container.Add("env", env); - foreach (var kvp in service.Service.Environment) + foreach (var kvp in project.EnvironmentVariables) { env.Add(new YamlMappingNode() { - { "name", kvp.Key }, - { "value", new YamlScalarNode(kvp.Value.ToString()) { Style = ScalarStyle.SingleQuoted, } }, + { "name", kvp.Name }, + { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } @@ -203,18 +194,18 @@ namespace Microsoft.Tye var volumeMount = new YamlMappingNode(); volumeMounts.Add(volumeMount); - volumeMount.Add("name", $"{binding.Service.Service.Name}-{binding.Binding.Name}"); - volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Service.Name}-{binding.Binding.Name}"); + volumeMount.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); + volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); volumeMount.Add("readOnly", "true"); } } - if (service.Service.Bindings.Count > 0) + if (project.Bindings.Count > 0) { var ports = new YamlSequenceNode(); container.Add("ports", ports); - foreach (var binding in service.Service.Bindings) + foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { @@ -242,7 +233,7 @@ namespace Microsoft.Tye { var volume = new YamlMappingNode(); volumes.Add(volume); - volume.Add("name", $"{binding.Service.Service.Name}-{binding.Binding.Name}"); + volume.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); var secret = new YamlMappingNode(); volume.Add("secret", secret); @@ -258,7 +249,7 @@ namespace Microsoft.Tye } } - return new KubernetesDeploymentOutput(service.Service.Name, new YamlDocument(root)); + return new KubernetesDeploymentOutput(project.Name, new YamlDocument(root)); } } } diff --git a/src/Microsoft.Tye.Core/OamApplicationGenerator.cs b/src/Microsoft.Tye.Core/OamApplicationGenerator.cs index b3dd4454..cf0fada1 100644 --- a/src/Microsoft.Tye.Core/OamApplicationGenerator.cs +++ b/src/Microsoft.Tye.Core/OamApplicationGenerator.cs @@ -13,7 +13,7 @@ namespace Microsoft.Tye { internal static class OamApplicationGenerator { - public static Task WriteOamApplicationAsync(TextWriter writer, OutputContext output, Application application, string applicationName, string environment) + public static Task WriteOamApplicationAsync(TextWriter writer, OutputContext output, ApplicationBuilder application, string environment) { if (writer is null) { @@ -30,11 +30,6 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(application)); } - if (applicationName is null) - { - throw new ArgumentNullException(nameof(applicationName)); - } - if (environment is null) { throw new ArgumentNullException(nameof(environment)); @@ -45,10 +40,7 @@ namespace Microsoft.Tye foreach (var service in application.Services) { - if (service.AppliesToEnvironment(environment)) - { - componentManifests.AddRange(service.Outputs.OfType()); - } + componentManifests.AddRange(service.Outputs.OfType()); } var root = new YamlMappingNode(); @@ -58,7 +50,7 @@ namespace Microsoft.Tye var metadata = new YamlMappingNode(); root.Add("metadata", metadata); - metadata.Add("name", applicationName.ToLowerInvariant()); + metadata.Add("name", application.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); diff --git a/src/Microsoft.Tye.Core/OamComponentGenerator.cs b/src/Microsoft.Tye.Core/OamComponentGenerator.cs index d2e98bbb..8dab0ca6 100644 --- a/src/Microsoft.Tye.Core/OamComponentGenerator.cs +++ b/src/Microsoft.Tye.Core/OamComponentGenerator.cs @@ -10,7 +10,7 @@ namespace Microsoft.Tye { internal static class OamComponentGenerator { - public static OamComponentOutput CreateOamComponent(OutputContext output, Application application, ServiceEntry service) + public static OamComponentOutput CreateOamComponent(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project) { if (output is null) { @@ -22,9 +22,9 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(application)); } - if (service is null) + if (project is null) { - throw new ArgumentNullException(nameof(service)); + throw new ArgumentNullException(nameof(project)); } var root = new YamlMappingNode(); @@ -34,7 +34,7 @@ namespace Microsoft.Tye var metadata = new YamlMappingNode(); root.Add("metadata", metadata); - metadata.Add("name", service.Service.Name); + metadata.Add("name", project.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); @@ -43,16 +43,16 @@ namespace Microsoft.Tye var containers = new YamlSequenceNode(); spec.Add("containers", containers); - var images = service.Outputs.OfType(); + var images = project.Outputs.OfType(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); - container.Add("name", service.Service.Name); // NOTE: to really support multiple images we'd need to generate unique names. + container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); } - return new OamComponentOutput(service.Service.Name, new YamlDocument(root)); + return new OamComponentOutput(project.Name, new YamlDocument(root)); } } } diff --git a/src/Microsoft.Tye.Core/ProjectReader.cs b/src/Microsoft.Tye.Core/ProjectReader.cs index 326af38b..c8ca2c55 100644 --- a/src/Microsoft.Tye.Core/ProjectReader.cs +++ b/src/Microsoft.Tye.Core/ProjectReader.cs @@ -59,7 +59,7 @@ namespace Microsoft.Tye } } - public static Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project) + public static Task ReadProjectDetailsAsync(OutputContext output, ProjectServiceBuilder project) { if (output is null) { @@ -71,9 +71,9 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(project)); } - EnsureMSBuildRegistered(output, projectFile); + EnsureMSBuildRegistered(output, project.ProjectFile); - EvaluateProject(output, projectFile, project); + EvaluateProject(output, project); if (!SemVersion.TryParse(project.Version, out var version)) { @@ -131,9 +131,16 @@ namespace Microsoft.Tye } } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void LogIt(OutputContext output) + { + output.WriteDebugLine("Loaded: " + typeof(ProjectInstance).Assembly.FullName); + output.WriteDebugLine("Loaded From: " + typeof(ProjectInstance).Assembly.Location); + } + // Do not load MSBuild types before using EnsureMSBuildRegistered. [MethodImpl(MethodImplOptions.NoInlining)] - private static void EvaluateProject(OutputContext output, FileInfo projectFile, Project project) + private static void EvaluateProject(OutputContext output, ProjectServiceBuilder project) { var sw = Stopwatch.StartNew(); @@ -145,17 +152,17 @@ namespace Microsoft.Tye try { - output.WriteDebugLine($"Loading project '{projectFile.FullName}'."); - var msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(projectFile.FullName, new ProjectOptions() + output.WriteDebugLine($"Loading project '{project.ProjectFile.FullName}'."); + var msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(project.ProjectFile.FullName, new ProjectOptions() { ProjectCollection = projectCollection, }); projectInstance = msbuildProject.CreateProjectInstance(); - output.WriteDebugLine($"Loaded project '{projectFile.FullName}'."); + output.WriteDebugLine($"Loaded project '{project.ProjectFile.FullName}'."); } catch (Exception ex) { - throw new CommandException($"Failed to load project: '{projectFile.FullName}'.", ex); + throw new CommandException($"Failed to load project: '{project.ProjectFile.FullName}'.", ex); } // Currently we only log at debug level. diff --git a/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs new file mode 100644 index 00000000..65601dea --- /dev/null +++ b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs @@ -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 EnvironmentVariables { get; } = new List(); + + // Used when running in a container locally. + public List Volumes { get; } = new List(); + } +} diff --git a/src/Microsoft.Tye.Core/PublishProjectStep.cs b/src/Microsoft.Tye.Core/PublishProjectStep.cs index c951e79d..27a49598 100644 --- a/src/Microsoft.Tye.Core/PublishProjectStep.cs +++ b/src/Microsoft.Tye.Core/PublishProjectStep.cs @@ -13,7 +13,7 @@ namespace Microsoft.Tye { public override string DisplayText => "Publishing Project..."; - public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutProject(output, service, out var project)) { @@ -30,16 +30,15 @@ namespace Microsoft.Tye return; } - var projectFilePath = Path.Combine(application.RootDirectory, project.RelativeFilePath); - var outputDirectory = Path.Combine(Path.GetDirectoryName(projectFilePath)!, "bin", "Release", project.TargetFramework, "publish"); + var outputDirectory = Path.Combine(project.ProjectFile.DirectoryName, "bin", "Release", project.TargetFramework, "publish"); output.WriteDebugLine("Running 'dotnet publish'."); - output.WriteCommandLine("dotnet", $"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\""); + output.WriteCommandLine("dotnet", $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory}\""); var capture = output.Capture(); var exitCode = await Process.ExecuteAsync( $"dotnet", - $"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\"", - application.GetProjectDirectory(project), + $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory}\"", + project.ProjectFile.DirectoryName, stdOut: capture.StdOut, stdErr: capture.StdErr); diff --git a/src/Microsoft.Tye.Core/PushDockerImageStep.cs b/src/Microsoft.Tye.Core/PushDockerImageStep.cs index 5c5e740b..300f6187 100644 --- a/src/Microsoft.Tye.Core/PushDockerImageStep.cs +++ b/src/Microsoft.Tye.Core/PushDockerImageStep.cs @@ -13,7 +13,7 @@ namespace Microsoft.Tye public string Environment { get; set; } = "production"; - public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutProject(output, service, out var _)) { @@ -25,11 +25,6 @@ namespace Microsoft.Tye return; } - if (SkipForEnvironment(output, service, Environment)) - { - return; - } - foreach (var image in service.Outputs.OfType()) { await DockerPush.ExecuteAsync(output, image.ImageName, image.ImageTag); diff --git a/src/Microsoft.Tye.Core/ServiceBuilder.cs b/src/Microsoft.Tye.Core/ServiceBuilder.cs new file mode 100644 index 00000000..95ede542 --- /dev/null +++ b/src/Microsoft.Tye.Core/ServiceBuilder.cs @@ -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 Bindings { get; } = new List(); + + // TODO: this is temporary while refactoring + public List Outputs { get; } = new List(); + } +} diff --git a/src/Microsoft.Tye.Core/ServiceExecutor.cs b/src/Microsoft.Tye.Core/ServiceExecutor.cs index fc1f09f8..0d3f4159 100644 --- a/src/Microsoft.Tye.Core/ServiceExecutor.cs +++ b/src/Microsoft.Tye.Core/ServiceExecutor.cs @@ -13,10 +13,10 @@ namespace Microsoft.Tye public sealed class ServiceExecutor { private readonly OutputContext output; - private readonly Application application; + private readonly ApplicationBuilder application; private readonly Step[] steps; - public ServiceExecutor(OutputContext output, Application application, IEnumerable steps) + public ServiceExecutor(OutputContext output, ApplicationBuilder application, IEnumerable steps) { if (output is null) { @@ -38,9 +38,9 @@ namespace Microsoft.Tye this.steps = steps.ToArray(); } - public async Task ExecuteAsync(ServiceEntry service) + public async Task ExecuteAsync(ServiceBuilder service) { - using var tracker = output.BeginStep($"Processing Service '{service.FriendlyName}'..."); + using var tracker = output.BeginStep($"Processing Service '{service.Name}'..."); for (var i = 0; i < steps.Length; i++) { var step = steps[i]; @@ -56,20 +56,9 @@ namespace Microsoft.Tye { public abstract string DisplayText { get; } - public abstract Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service); + public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service); - protected bool SkipForEnvironment(OutputContext output, ServiceEntry service, string environment) - { - if (!service.AppliesToEnvironment(environment)) - { - output.WriteDebugLine($"Service '{service.FriendlyName}' is not part of environment '{environment}'. Skipping."); - return true; - } - - return false; - } - - protected bool SkipWithoutProject(OutputContext output, ServiceEntry service, [MaybeNullWhen(returnValue: true)] out Project project) + protected bool SkipWithoutProject(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ProjectServiceBuilder project) { if (output is null) { @@ -81,18 +70,18 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(service)); } - if (service.Service.Source is Project p) + if (service is ProjectServiceBuilder p) { project = p; return false; } - output.WriteInfoLine($"Service '{service.FriendlyName}' does not have a project associated. Skipping."); + output.WriteInfoLine($"Service '{service.Name}' does not have a project associated. Skipping."); project = default!; return true; } - protected bool SkipWithoutContainerInfo(OutputContext output, ServiceEntry service, [MaybeNullWhen(returnValue: true)] out ContainerInfo container) + protected bool SkipWithoutContainerInfo(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ContainerInfo container) { if (output is null) { @@ -104,18 +93,18 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(service)); } - if (service.Service.GeneratedAssets.Container is ContainerInfo c) + if (service is ProjectServiceBuilder project && project.ContainerInfo is ContainerInfo c) { container = c; return false; } - output.WriteInfoLine($"Service '{service.FriendlyName}' does not produce a container. Skipping."); + output.WriteInfoLine($"Service '{service.Name}' does not produce a container. Skipping."); container = default!; return true; } - protected bool SkipWithoutContainerOutput(OutputContext output, ServiceEntry service) + protected bool SkipWithoutContainerOutput(OutputContext output, ServiceBuilder service) { if (output is null) { @@ -132,7 +121,7 @@ namespace Microsoft.Tye return false; } - output.WriteInfoLine($"Service '{service.FriendlyName}' does not have a container. Skipping."); + output.WriteInfoLine($"Service '{service.Name}' does not have a container. Skipping."); return true; } } diff --git a/src/Microsoft.Tye.Core/ServiceOutput.cs b/src/Microsoft.Tye.Core/ServiceOutput.cs new file mode 100644 index 00000000..6c509710 --- /dev/null +++ b/src/Microsoft.Tye.Core/ServiceOutput.cs @@ -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 + { + } +} diff --git a/src/Microsoft.Tye.Core/ValidateSecretStep.cs b/src/Microsoft.Tye.Core/ValidateSecretStep.cs index af677f53..512806d4 100644 --- a/src/Microsoft.Tye.Core/ValidateSecretStep.cs +++ b/src/Microsoft.Tye.Core/ValidateSecretStep.cs @@ -31,7 +31,7 @@ namespace Microsoft.Tye // Keep track of secrets we've seen so we don't validate them twice. public HashSet Secrets { get; } = new HashSet(); - public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { var bindings = service.Outputs.OfType().FirstOrDefault(); if (bindings is null) @@ -86,17 +86,17 @@ namespace Microsoft.Tye if (!Interactive) { throw new CommandException( - $"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Service.Name}' is missing from the deployment environment. " + + $"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Name}' is missing from the deployment environment. " + $"Rerun the command with --interactive to specify the value interactively, or with --force to skip validation. Alternatively " + $"use the following command to manually create the secret." + System.Environment.NewLine + $"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring="); } // If we get here then we should create the sceret. - var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Service.Name}'", allowEmpty: true); + var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Name}'", allowEmpty: true); if (string.IsNullOrWhiteSpace(text)) { - output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.Service.Name}'. This may prevent creation of pods until secrets are created."); + output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created."); output.WriteAlways($"Manually create a secret with:"); output.WriteAlways($"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring="); continue; @@ -128,7 +128,7 @@ namespace Microsoft.Tye var yaml = service.Outputs.OfType().ToArray(); if (yaml.Length == 0) { - output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping."); + output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping."); return; } @@ -163,7 +163,7 @@ namespace Microsoft.Tye throw new CommandException("'kubectl apply' failed."); } - output.WriteInfoLine($"Deployed service '{service.FriendlyName}'."); + output.WriteInfoLine($"Deployed service '{service.Name}'."); } } } diff --git a/src/Microsoft.Tye.Core/VolumeBuilder.cs b/src/Microsoft.Tye.Core/VolumeBuilder.cs new file mode 100644 index 00000000..2641f087 --- /dev/null +++ b/src/Microsoft.Tye.Core/VolumeBuilder.cs @@ -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; } + } +} diff --git a/src/Microsoft.Tye.Core/WriteServiceYamlStep.cs b/src/Microsoft.Tye.Core/WriteServiceYamlStep.cs index 28cc052f..c5b8d603 100644 --- a/src/Microsoft.Tye.Core/WriteServiceYamlStep.cs +++ b/src/Microsoft.Tye.Core/WriteServiceYamlStep.cs @@ -18,20 +18,20 @@ namespace Microsoft.Tye public DirectoryInfo OutputDirectory { get; set; } = default!; - public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) + public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { var yaml = service.Outputs.OfType().ToArray(); if (yaml.Length == 0) { - output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping."); + output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping."); return Task.CompletedTask; } - var outputFilePath = Path.Combine(OutputDirectory.FullName, $"{service.Service.Name}.yaml"); + var outputFilePath = Path.Combine(OutputDirectory.FullName, $"{service.Name}.yaml"); output.WriteInfoLine($"Writing output to '{outputFilePath}'."); if (File.Exists(outputFilePath) && !Force) { - throw new CommandException($"'{service.Service.Name}.yaml' already exists for project. use '--force' to overwrite."); + throw new CommandException($"'{service.Name}.yaml' already exists for project. use '--force' to overwrite."); } File.Delete(outputFilePath); diff --git a/src/tye/ConfigModel/ConfigApplication.cs b/src/tye/ApplicationBuilderExtensions.cs similarity index 55% rename from src/tye/ConfigModel/ConfigApplication.cs rename to src/tye/ApplicationBuilderExtensions.cs index 8ec6622e..ab4658e1 100644 --- a/src/tye/ConfigModel/ConfigApplication.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -4,61 +4,68 @@ using System; using System.Collections.Generic; -using System.IO; using Microsoft.Tye.Hosting.Model; -using YamlDotNet.Serialization; namespace Microsoft.Tye.ConfigModel { - public class ConfigApplication + public static class ApplicationBuilderExtensions { - // 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 Ingress { get; set; } = new List(); - - public List Services { get; set; } = new List(); - - public Tye.Hosting.Model.Application ToHostingApplication() + public static Tye.Hosting.Model.Application ToHostingApplication(this ApplicationBuilder application) { var services = new Dictionary(); - foreach (var service in Services) + foreach (var service in application.Services) { RunInfo? runInfo; - if (service.External) + int replicas; + var env = new List(); + if (service is ExternalServiceBuilder) { runInfo = null; + replicas = 1; } - else if (service.Image is object) + else if (service is ContainerServiceBuilder container) { - var dockerRunInfo = new DockerRunInfo(service.Image, service.Args); + var dockerRunInfo = new DockerRunInfo(container.Image, container.Args); - foreach (var mapping in service.Volumes) + foreach (var mapping in container.Volumes) { dockerRunInfo.VolumeMappings[mapping.Source!] = mapping.Target!; } runInfo = dockerRunInfo; + replicas = container.Replicas; + + foreach (var entry in container.EnvironmentVariables) + { + env.Add(new ConfigurationSource(entry.Name, entry.Value)); + } } - else if (service.Executable is object) + else if (service is ExecutableServiceBuilder executable) { - runInfo = new ExecutableRunInfo(service.Executable, service.WorkingDirectory, service.Args); + runInfo = new ExecutableRunInfo(executable.Executable, executable.WorkingDirectory, executable.Args); + replicas = executable.Replicas; + + foreach (var entry in executable.EnvironmentVariables) + { + env.Add(new ConfigurationSource(entry.Name, entry.Value)); + } } - else if (service.Project is object) + else if (service is ProjectServiceBuilder project) { - var projectInfo = new ProjectRunInfo(service.Project, service.Args, service.Build ?? true); + var projectInfo = new ProjectRunInfo(project.ProjectFile.FullName, project.Args, project.Build); - foreach (var mapping in service.Volumes) + foreach (var mapping in project.Volumes) { projectInfo.VolumeMappings[mapping.Source!] = mapping.Target!; } runInfo = projectInfo; + replicas = project.Replicas; + + foreach (var entry in project.EnvironmentVariables) + { + env.Add(new ConfigurationSource(entry.Name, entry.Value)); + } } else { @@ -67,8 +74,9 @@ namespace Microsoft.Tye.ConfigModel var description = new Tye.Hosting.Model.ServiceDescription(service.Name, runInfo) { - Replicas = service.Replicas ?? 1, + Replicas = replicas, }; + description.Configuration.AddRange(env); foreach (var binding in service.Bindings) { @@ -84,28 +92,24 @@ namespace Microsoft.Tye.ConfigModel }); } - foreach (var entry in service.Configuration) - { - description.Configuration.Add(new ConfigurationSource(entry.Name, entry.Value)); - } - services.Add(service.Name, new Tye.Hosting.Model.Service(description)); } - foreach (var ingress in Ingress) + // Ingress get turned into services for hosting + foreach (var ingress in application.Ingress) { - var rules = new List(); + var rules = new List(); foreach (var rule in ingress.Rules) { - rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service!)); + rules.Add(new Tye.Hosting.Model.IngressRule(rule.Host, rule.Path, rule.Service!)); } var runInfo = new IngressRunInfo(rules); var description = new Tye.Hosting.Model.ServiceDescription(ingress.Name, runInfo) { - Replicas = ingress.Replicas ?? 1, + Replicas = ingress.Replicas, }; foreach (var binding in ingress.Bindings) @@ -122,7 +126,7 @@ namespace Microsoft.Tye.ConfigModel services.Add(ingress.Name, new Tye.Hosting.Model.Service(description)); } - return new Tye.Hosting.Model.Application(Source, services); + return new Tye.Hosting.Model.Application(application.Source, services); } } } diff --git a/src/tye/BuildHost.cs b/src/tye/BuildHost.cs index d2da4e72..cf13a794 100644 --- a/src/tye/BuildHost.cs +++ b/src/tye/BuildHost.cs @@ -5,23 +5,21 @@ using System.Collections.Generic; using System.CommandLine; using System.IO; -using System.Text; using System.Threading.Tasks; -using Microsoft.Tye.ConfigModel; namespace Microsoft.Tye { public static class BuildHost { - public static Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive) + public static async Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive) { - var application = ConfigFactory.FromFile(path); - return ExecuteBuildAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); + var output = new OutputContext(console, verbosity); + var application = await ApplicationFactory.CreateAsync(output, path); + await ExecuteBuildAsync(output, application, environment: "production", interactive); } - public static async Task ExecuteBuildAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) + public static async Task ExecuteBuildAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive) { - var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: false); var steps = new List() { new CombineStep() { Environment = environment, }, @@ -29,8 +27,10 @@ namespace Microsoft.Tye new BuildDockerImageStep() { Environment = environment, }, }; - var executor = new ServiceExecutor(output, temporaryApplication, steps); - foreach (var service in temporaryApplication.Services) + Program.ApplyRegistryAndDefaults(output, application, interactive, requireRegistry: false); + + var executor = new ServiceExecutor(output, application, steps); + foreach (var service in application.Services) { await executor.ExecuteAsync(service); } diff --git a/src/tye/GenerateHost.cs b/src/tye/GenerateHost.cs index 2952d1b5..26922470 100644 --- a/src/tye/GenerateHost.cs +++ b/src/tye/GenerateHost.cs @@ -7,21 +7,20 @@ using System.CommandLine; using System.IO; using System.Text; using System.Threading.Tasks; -using Microsoft.Tye.ConfigModel; namespace Microsoft.Tye { public static class GenerateHost { - public static Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive) + public static async Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive) { - var application = ConfigFactory.FromFile(path); - return ExecuteGenerateAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); + var output = new OutputContext(console, verbosity); + var application = await ApplicationFactory.CreateAsync(output, path); + await ExecuteGenerateAsync(output, application, environment: "production", interactive); } - public static async Task ExecuteGenerateAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) + public static async Task ExecuteGenerateAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive) { - var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: false); var steps = new List() { new CombineStep() { Environment = environment, }, @@ -31,20 +30,22 @@ namespace Microsoft.Tye steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, }); - var executor = new ServiceExecutor(output, temporaryApplication, steps); - foreach (var service in temporaryApplication.Services) + Program.ApplyRegistryAndDefaults(output, application, interactive, requireRegistry: false); + + var executor = new ServiceExecutor(output, application, steps); + foreach (var service in application.Services) { await executor.ExecuteAsync(service); } - await GenerateApplicationManifestAsync(output, temporaryApplication, application.Name ?? application.Source.Directory.Name, environment); + await GenerateApplicationManifestAsync(output, application, environment); } - private static async Task GenerateApplicationManifestAsync(OutputContext output, Tye.Application application, string applicationName, string environment) + private static async Task GenerateApplicationManifestAsync(OutputContext output, ApplicationBuilder application, string environment) { using var step = output.BeginStep("Generating Application Manifests..."); - var outputFilePath = Path.GetFullPath(Path.Combine(application.RootDirectory, $"{applicationName}-generate-{environment}.yaml")); + var outputFilePath = Path.GetFullPath(Path.Combine(application.Source.DirectoryName, $"{application.Name}-generate-{environment}.yaml")); output.WriteInfoLine($"Writing output to '{outputFilePath}'."); { File.Delete(outputFilePath); diff --git a/src/tye/InitHost.cs b/src/tye/InitHost.cs index c5ec45bc..84c47694 100644 --- a/src/tye/InitHost.cs +++ b/src/tye/InitHost.cs @@ -71,7 +71,6 @@ services: { // If the input file is a project or solution then use that as the name application.Name = Path.GetFileNameWithoutExtension(path.Name).ToLowerInvariant(); - application.Ingress = null!; foreach (var service in application.Services) diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index 31f6cf2e..57a61a91 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -29,7 +29,7 @@ namespace Microsoft.Tye Required = false }); - command.Handler = CommandHandler.Create((console, path, verbosity, interactive, force) => + command.Handler = CommandHandler.Create(async (console, path, verbosity, interactive, force) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) @@ -37,14 +37,15 @@ namespace Microsoft.Tye throw new CommandException("No project or solution file was found."); } - var application = ConfigFactory.FromFile(path); - return ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive, force); + var output = new OutputContext(console, verbosity); + var application = await ApplicationFactory.CreateAsync(output, path); + await ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive, force); }); return command; } - private static async Task ExecuteDeployAsync(OutputContext output, ConfigApplication application, string environment, bool interactive, bool force) + private static async Task ExecuteDeployAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive, bool force) { if (!await KubectlDetector.Instance.IsKubectlInstalled.Value) { @@ -56,7 +57,6 @@ namespace Microsoft.Tye throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster."); } - var temporaryApplication = await CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: true); var steps = new List() { new CombineStep() { Environment = environment, }, @@ -69,125 +69,18 @@ namespace Microsoft.Tye steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, }); steps.Add(new DeployServiceYamlStep() { Environment = environment, }); - var executor = new ServiceExecutor(output, temporaryApplication, steps); - foreach (var service in temporaryApplication.Services) - { - await executor.ExecuteAsync(service); - } - - await DeployApplicationManifestAsync(output, temporaryApplication, application.Source.Directory.Name, environment); - } - - internal static async Task CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive, bool requireRegistry) - { - var globals = new ApplicationGlobals() - { - Name = application.Name, - Registry = application.Registry is null ? null : new ContainerRegistry(application.Registry), - }; - - var services = new List(); - foreach (var configService in application.Services) - { - if (configService.Project is string projectFile) - { - var fullPathProjectFile = Path.Combine(application.Source.DirectoryName, projectFile); - var project = new Project(fullPathProjectFile); - var service = new Service(configService.Name) - { - Source = project, - }; - - service.Replicas = configService.Replicas ?? 1; - - foreach (var configBinding in configService.Bindings) - { - var binding = new ServiceBinding(configBinding.Name ?? service.Name) - { - ConnectionString = configBinding.ConnectionString, - Host = configBinding.Host, - Port = configBinding.Port, - Protocol = configBinding.Protocol, - }; - - binding.Protocol ??= "http"; - - if (binding.Port == null && configBinding.AutoAssignPort) - { - if (binding.Protocol == "http" || binding.Protocol == null) - { - binding.Port = 80; - } - else if (binding.Protocol == "https") - { - binding.Port = 443; - } - } - - service.Bindings.Add(binding); - } - - var serviceEntry = new ServiceEntry(service, configService.Name); - - await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(fullPathProjectFile), project); - - var container = new ContainerInfo() - { - UseMultiphaseDockerfile = false, - }; - service.GeneratedAssets.Container = container; - services.Add(serviceEntry); - } - else - { - // For a non-project, we don't really need much info about it, just the name and bindings - var service = new Service(configService.Name); - foreach (var configBinding in configService.Bindings) - { - service.Bindings.Add(new ServiceBinding(configBinding.Name ?? service.Name) - { - ConnectionString = configBinding.ConnectionString, - Host = configBinding.Host, - Port = configBinding.Port, - Protocol = configBinding.Protocol, - }); - } - - var serviceEntry = new ServiceEntry(service, configService.Name); - services.Add(serviceEntry); - } - } - - var temporaryApplication = new TemporaryApplicationAdapter(application, globals, services); - if (temporaryApplication.Globals.Registry?.Hostname == null && interactive) - { - var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry); - if (!string.IsNullOrWhiteSpace(registry)) - { - temporaryApplication.Globals.Registry = new ContainerRegistry(registry.Trim()); - } - } - else if (temporaryApplication.Globals.Registry?.Hostname == null && requireRegistry) - { - throw new CommandException("A registry is required for deploy operations. Add the registry to 'tye.yaml' or use '-i' for interactive mode."); - } - else - { - // No registry specified, and that's OK! - } + ApplyRegistryAndDefaults(output, application, interactive, requireRegistry: true); - foreach (var service in temporaryApplication.Services) + var executor = new ServiceExecutor(output, application, steps); + foreach (var service in application.Services) { - if (service.Service.Source is Project project && service.Service.GeneratedAssets.Container is ContainerInfo container) - { - DockerfileGenerator.ApplyContainerDefaults(temporaryApplication, service, project, container); - } + await executor.ExecuteAsync(service); } - return temporaryApplication; + await DeployApplicationManifestAsync(output, application, application.Source.Directory.Name, environment); } - private static async Task DeployApplicationManifestAsync(OutputContext output, Tye.Application application, string applicationName, string environment) + private static async Task DeployApplicationManifestAsync(OutputContext output, ApplicationBuilder application, string applicationName, string environment) { using var step = output.BeginStep("Deploying Application Manifests..."); @@ -221,5 +114,33 @@ namespace Microsoft.Tye step.MarkComplete(); } + + internal static void ApplyRegistryAndDefaults(OutputContext output, ApplicationBuilder application, bool interactive, bool requireRegistry) + { + if (application.Registry is null && interactive) + { + var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry); + if (!string.IsNullOrWhiteSpace(registry)) + { + application.Registry = new ContainerRegistry(registry.Trim()); + } + } + else if (application.Registry is null && requireRegistry) + { + throw new CommandException("A registry is required for deploy operations. Add the registry to 'tye.yaml' or use '-i' for interactive mode."); + } + else + { + // No registry specified, and that's OK! + } + + foreach (var service in application.Services) + { + if (service is ProjectServiceBuilder project && project.ContainerInfo is ContainerInfo container) + { + DockerfileGenerator.ApplyContainerDefaults(application, project, container); + } + } + } } } diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index 9c9d82e1..bb44b1e0 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -70,7 +70,8 @@ namespace Microsoft.Tye throw new CommandException("No project or solution file was found."); } - var application = ConfigFactory.FromFile(path); + var output = new OutputContext(console, Verbosity.Quiet); + var application = await ApplicationFactory.CreateAsync(output, path); var serviceCount = application.Services.Count; InitializeThreadPoolSettings(serviceCount); diff --git a/src/tye/TemporaryApplicationAdapter.cs b/src/tye/TemporaryApplicationAdapter.cs deleted file mode 100644 index b1cd5a3d..00000000 --- a/src/tye/TemporaryApplicationAdapter.cs +++ /dev/null @@ -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 services) - { - this.application = application; - Globals = globals; - Services = services; - } - - public override ApplicationGlobals Globals { get; } - - public override string RootDirectory => application.Source.DirectoryName; - - public override IReadOnlyList Services { get; } - } -} diff --git a/test/E2ETest/TyeBuildTests.cs b/test/E2ETest/TyeBuildTests.cs index 593a7d72..16420b59 100644 --- a/test/E2ETest/TyeBuildTests.cs +++ b/test/E2ETest/TyeBuildTests.cs @@ -5,8 +5,6 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Tye; -using Microsoft.Tye.ConfigModel; -using Xunit; using Xunit.Abstractions; namespace E2ETest @@ -37,13 +35,14 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); - application.Registry = "test"; + application.Registry = new ContainerRegistry("test"); try { - await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false); await DockerAssert.AssertImageExistsAsync(output, "test/test-project"); } @@ -69,13 +68,14 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); - application.Registry = "test"; + application.Registry = new ContainerRegistry("test"); try { - await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false); await DockerAssert.AssertImageExistsAsync(output, "test/backend"); await DockerAssert.AssertImageExistsAsync(output, "test/frontend"); @@ -104,13 +104,14 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); - application.Registry = "test"; + application.Registry = new ContainerRegistry("test"); try { - await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false); await DockerAssert.AssertImageExistsAsync(output, "test/backend"); await DockerAssert.AssertImageExistsAsync(output, "test/frontend"); @@ -138,12 +139,12 @@ namespace E2ETest DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); try { - await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false); await DockerAssert.AssertImageExistsAsync(output, "test-project"); } diff --git a/test/E2ETest/TyeGenerateTests.cs b/test/E2ETest/TyeGenerateTests.cs index 811e6525..3076c4f6 100644 --- a/test/E2ETest/TyeGenerateTests.cs +++ b/test/E2ETest/TyeGenerateTests.cs @@ -37,14 +37,15 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); // Need to add docker registry for generate - application.Registry = "test"; + application.Registry = new ContainerRegistry("test"); try { - await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false); // name of application is the folder var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); @@ -75,14 +76,15 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); // Need to add docker registry for generate - application.Registry = "test"; + application.Registry = new ContainerRegistry("test"); try { - await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false); // name of application is the folder var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); @@ -90,8 +92,6 @@ namespace E2ETest Assert.Equal(expectedContent, content); - await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); - await DockerAssert.AssertImageExistsAsync(output, "test/backend"); await DockerAssert.AssertImageExistsAsync(output, "test/frontend"); } @@ -119,14 +119,15 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); // Need to add docker registry for generate - application.Registry = "test"; + application.Registry = new ContainerRegistry("test"); try { - await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false); // name of application is the folder var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); @@ -161,11 +162,12 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - var application = ConfigFactory.FromFile(projectFile); + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); try { - await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); + await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false); // name of application is the folder var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); @@ -173,8 +175,6 @@ namespace E2ETest Assert.Equal(expectedContent, content); - await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false); - await DockerAssert.AssertImageExistsAsync(output, "test-project"); } finally diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 10adcea0..99e5db7f 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -50,7 +49,9 @@ namespace E2ETest DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj")); - using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty()) + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + using var host = new TyeHost(application.ToHostingApplication(), Array.Empty()) { Sink = sink, }; @@ -108,7 +109,9 @@ namespace E2ETest DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty()) + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + using var host = new TyeHost(application.ToHostingApplication(), Array.Empty()) { Sink = sink, }; @@ -143,7 +146,9 @@ namespace E2ETest DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty()) + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + using var host = new TyeHost(application.ToHostingApplication(), Array.Empty()) { Sink = sink, }; @@ -211,7 +216,9 @@ namespace E2ETest DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); - using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), new[] { "--docker" }) + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + using var host = new TyeHost(application.ToHostingApplication(), new[] { "--docker" }) { Sink = sink, }; @@ -251,7 +258,9 @@ namespace E2ETest var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj")); // Debug targets can be null if not specified, so make sure calling host.Start does not throw. - using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty(), null!) + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + using var host = new TyeHost(application.ToHostingApplication(), Array.Empty()) { Sink = sink, }; diff --git a/test/E2ETest/testassets/generate/frontend-backend.yaml b/test/E2ETest/testassets/generate/frontend-backend.yaml index a4e061e9..333f824d 100644 --- a/test/E2ETest/testassets/generate/frontend-backend.yaml +++ b/test/E2ETest/testassets/generate/frontend-backend.yaml @@ -47,7 +47,7 @@ spec: app.kubernetes.io/name: backend type: ClusterIP ports: - - name: backend + - name: http protocol: TCP port: 80 targetPort: 80 @@ -102,7 +102,7 @@ spec: app.kubernetes.io/name: frontend type: ClusterIP ports: - - name: frontend + - name: http protocol: TCP port: 80 targetPort: 80 diff --git a/test/E2ETest/testassets/generate/multi-project.yaml b/test/E2ETest/testassets/generate/multi-project.yaml index 7734f44e..988a636c 100644 --- a/test/E2ETest/testassets/generate/multi-project.yaml +++ b/test/E2ETest/testassets/generate/multi-project.yaml @@ -58,7 +58,7 @@ spec: app.kubernetes.io/name: backend type: ClusterIP ports: - - name: backend + - name: http protocol: TCP port: 7000 targetPort: 7000 @@ -124,7 +124,7 @@ spec: app.kubernetes.io/name: frontend type: ClusterIP ports: - - name: frontend + - name: http protocol: TCP port: 8000 targetPort: 8000 diff --git a/test/E2ETest/testassets/generate/single-project-noregistry.yaml b/test/E2ETest/testassets/generate/single-project-noregistry.yaml index 9e33747f..07e979f3 100644 --- a/test/E2ETest/testassets/generate/single-project-noregistry.yaml +++ b/test/E2ETest/testassets/generate/single-project-noregistry.yaml @@ -41,7 +41,7 @@ spec: app.kubernetes.io/name: test-project type: ClusterIP ports: - - name: test-project + - name: http protocol: TCP port: 80 targetPort: 80 diff --git a/test/E2ETest/testassets/generate/single-project.yaml b/test/E2ETest/testassets/generate/single-project.yaml index 70c70507..d543a9b8 100644 --- a/test/E2ETest/testassets/generate/single-project.yaml +++ b/test/E2ETest/testassets/generate/single-project.yaml @@ -41,7 +41,7 @@ spec: app.kubernetes.io/name: test-project type: ClusterIP ports: - - name: test-project + - name: http protocol: TCP port: 80 targetPort: 80