diff --git a/samples/multi-project/tye.yaml b/samples/multi-project/tye.yaml index 328f37f6..0ee906d4 100644 --- a/samples/multi-project/tye.yaml +++ b/samples/multi-project/tye.yaml @@ -4,16 +4,14 @@ name: multi-project services: - name: backend project: backend/backend.csproj - bindings: - - port: 7000 - name: frontend project: frontend/frontend.csproj replicas: 2 - bindings: - - port: 8000 - name: worker project: worker/worker.csproj - name: rabbit image: rabbitmq:3-management bindings: - port: 5672 +dependencies: + - path: ../single-project/tye.yaml \ No newline at end of file diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 46b07dba..822e42bc 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -21,208 +21,229 @@ namespace Microsoft.Tye throw new ArgumentNullException(nameof(source)); } - var config = ConfigFactory.FromFile(source); - ValidateConfigApplication(config); + var queue = new Queue(); - 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 expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); - var projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, expandedProject)); - var project = new ProjectServiceBuilder(configService.Name, projectFile); - service = project; + var rootConfig = ConfigFactory.FromFile(source); + ValidateConfigApplication(rootConfig); + var root = new ApplicationBuilder(source, rootConfig.Name ?? source.Directory.Name.ToLowerInvariant()); - project.Build = configService.Build ?? true; - project.Args = configService.Args; - project.Replicas = configService.Replicas ?? 1; + queue.Enqueue(rootConfig); - await ProjectReader.ReadProjectDetailsAsync(output, project); + while (queue.Count > 0) + { + var config = queue.Dequeue(); - // We don't apply more container defaults here because we might need - // to promptly for the registry name. - project.ContainerInfo = new ContainerInfo() - { - UseMultiphaseDockerfile = false, - }; - } - else if (!string.IsNullOrEmpty(configService.Image)) + if (config == rootConfig && !string.IsNullOrEmpty(config.Registry)) { - var container = new ContainerServiceBuilder(configService.Name, configService.Image) - { - Args = configService.Args, - Replicas = configService.Replicas ?? 1 - }; - service = container; + root.Registry = new ContainerRegistry(config.Registry); } - else if (!string.IsNullOrEmpty(configService.Executable)) - { - var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); - var workingDirectory = ""; - // Special handling of .dlls as executables (it will be executed as dotnet {dll}) - if (Path.GetExtension(expandedExecutable) == ".dll") + foreach (var configService in config.Services) + { + ServiceBuilder service; + if (!string.IsNullOrEmpty(configService.Project)) { - expandedExecutable = Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, expandedExecutable)); - workingDirectory = Path.GetDirectoryName(expandedExecutable)!; - } + var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); + var projectFile = new FileInfo(Path.Combine(config.Source.DirectoryName, expandedProject)); - var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable) - { - Args = configService.Args, - WorkingDirectory = configService.WorkingDirectory != null ? - Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) : - workingDirectory, - 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."); - } + var project = new ProjectServiceBuilder(configService.Name, projectFile); + service = project; - builder.Services.Add(service); + project.Build = configService.Build ?? true; + project.Args = configService.Args; + project.Replicas = configService.Replicas ?? 1; - // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding - if (configService.Bindings.Count == 0 && - service is ProjectServiceBuilder project2 && - project2.IsAspNet) - { - // HTTP is the default binding - service.Bindings.Add(new BindingBuilder() - { - AutoAssignPort = true, - Protocol = "http" - }); + await ProjectReader.ReadProjectDetailsAsync(output, project); - service.Bindings.Add(new BindingBuilder() - { - Name = "https", - AutoAssignPort = true, - Protocol = "https" - }); - } - else - { - foreach (var configBinding in configService.Bindings) + // We don't apply more container defaults here because we might need + // to promptly for the registry name. + project.ContainerInfo = new ContainerInfo() + { + UseMultiphaseDockerfile = false, + }; + } + else if (!string.IsNullOrEmpty(configService.Image)) { - var binding = new BindingBuilder() + var container = new ContainerServiceBuilder(configService.Name, configService.Image) { - Name = configBinding.Name, - AutoAssignPort = configBinding.AutoAssignPort, - ConnectionString = configBinding.ConnectionString, - Host = configBinding.Host, - ContainerPort = configBinding.ContainerPort, - Port = configBinding.Port, - Protocol = configBinding.Protocol, + Args = configService.Args, + Replicas = configService.Replicas ?? 1 }; + service = container; + } + else if (!string.IsNullOrEmpty(configService.Executable)) + { + var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); + var workingDirectory = ""; - // Assume HTTP for projects only (containers may be different) - if (binding.ConnectionString == null && configService.Project != null) + // Special handling of .dlls as executables (it will be executed as dotnet {dll}) + if (Path.GetExtension(expandedExecutable) == ".dll") { - binding.Protocol ??= "http"; + expandedExecutable = Path.GetFullPath(Path.Combine(config.Source.Directory.FullName, expandedExecutable)); + workingDirectory = Path.GetDirectoryName(expandedExecutable)!; } - 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); + var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable) + { + Args = configService.Args, + WorkingDirectory = configService.WorkingDirectory != null ? + Path.GetFullPath(Path.Combine(config.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) : + workingDirectory, + Replicas = configService.Replicas ?? 1 + }; + service = executable; } - else if (service is ExternalServiceBuilder) + else if (configService.External) { - throw new CommandException("External services do not support environment variables."); + var external = new ExternalServiceBuilder(configService.Name); + service = external; } 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) + // There's no hierarchy, just add it to the list of services. + root.Services.Add(service); + + // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding + if (configService.Bindings.Count == 0 && + service is ProjectServiceBuilder project2 && + project2.IsAspNet) { - container.Volumes.Add(volume); + // HTTP is the default binding + service.Bindings.Add(new BindingBuilder() + { + AutoAssignPort = true, + Protocol = "http" + }); + + service.Bindings.Add(new BindingBuilder() + { + Name = "https", + AutoAssignPort = true, + Protocol = "https" + }); } - else if (service is ExecutableServiceBuilder executable) + else { - throw new CommandException("Executable services do not support volumes."); + foreach (var configBinding in configService.Bindings) + { + var binding = new BindingBuilder() + { + Name = configBinding.Name, + AutoAssignPort = configBinding.AutoAssignPort, + ConnectionString = configBinding.ConnectionString, + Host = configBinding.Host, + ContainerPort = configBinding.ContainerPort, + Port = configBinding.Port, + Protocol = configBinding.Protocol, + }; + + // Assume HTTP for projects only (containers may be different) + if (binding.ConnectionString == null && configService.Project != null) + { + binding.Protocol ??= "http"; + } + + service.Bindings.Add(binding); + } } - else if (service is ExternalServiceBuilder) + + foreach (var configEnvVar in configService.Configuration) { - throw new CommandException("External services do not support volumes."); + 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."); + } } - else + + foreach (var configVolume in configService.Volumes) { - throw new CommandException("Unable to determine service type."); + 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; + foreach (var configIngress in config.Ingress) + { + var ingress = new IngressBuilder(configIngress.Name); + ingress.Replicas = configIngress.Replicas ?? 1; - builder.Ingress.Add(ingress); + root.Ingress.Add(ingress); - foreach (var configBinding in configIngress.Bindings) - { - var binding = new IngressBindingBuilder() + foreach (var configBinding in configIngress.Bindings) { - AutoAssignPort = configBinding.AutoAssignPort, - Name = configBinding.Name, - Port = configBinding.Port, - Protocol = configBinding.Protocol ?? "http", - }; - ingress.Bindings.Add(binding); + 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); + } } - foreach (var configRule in configIngress.Rules) + foreach (var configDependency in config.Dependencies) { - var rule = new IngressRuleBuilder() - { - Host = configRule.Host, - Path = configRule.Path, - Service = configRule.Service, - }; - ingress.Rules.Add(rule); + // TODO: Validate the extension is yaml + var expandedPath = Environment.ExpandEnvironmentVariables(configDependency.Path); + var dependencyFile = new FileInfo(Path.Combine(config.Source.DirectoryName, expandedPath)); + var dependencyConfig = ConfigFactory.FromFile(dependencyFile); + ValidateConfigApplication(dependencyConfig); + queue.Enqueue(dependencyConfig); } } - return builder; + return root; } private static void ValidateConfigApplication(ConfigApplication config) diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs index 30ae1099..0450d771 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs @@ -26,5 +26,7 @@ namespace Microsoft.Tye.ConfigModel public List Services { get; set; } = new List(); public List Ingress { get; set; } = new List(); + + public List Dependencies { get; set; } = new List(); } } diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigDependency.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigDependency.cs new file mode 100644 index 00000000..2e42096d --- /dev/null +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigDependency.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace Microsoft.Tye.ConfigModel +{ + public class ConfigDependency + { + [Required] + public string? Path { get; set; } + } +}