diff --git a/samples/multi-project/shared/IOrderService.cs b/samples/multi-project/backend/IOrderService.cs similarity index 100% rename from samples/multi-project/shared/IOrderService.cs rename to samples/multi-project/backend/IOrderService.cs diff --git a/samples/multi-project/shared/Order.cs b/samples/multi-project/backend/Order.cs similarity index 100% rename from samples/multi-project/shared/Order.cs rename to samples/multi-project/backend/Order.cs diff --git a/samples/multi-project/backend/Program.cs b/samples/multi-project/backend/Program.cs index d2035a6f..b03fbf78 100644 --- a/samples/multi-project/backend/Program.cs +++ b/samples/multi-project/backend/Program.cs @@ -1,6 +1,9 @@ +using System; using System.Diagnostics; +using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; namespace Backend @@ -16,6 +19,14 @@ namespace Backend public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(config => + { + foreach (var directory in Directory.GetDirectories("/var/tye/bindings/")) + { + Console.WriteLine($"Adding config in '{directory}'."); + config.AddKeyPerFile(directory, optional: true); + } + }) .ConfigureWebHostDefaults(web => { web.UseStartup() diff --git a/samples/multi-project/backend/Startup.cs b/samples/multi-project/backend/Startup.cs index ca08de3a..87c488cc 100644 --- a/samples/multi-project/backend/Startup.cs +++ b/samples/multi-project/backend/Startup.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -25,10 +26,22 @@ namespace Backend services.AddSingleton(sp => { + AmqpTcpEndpoint endpoint; + var connectionString = Configuration["connectionstring:rabbit"]; + if (connectionString == null) + { + var host = Configuration["service:rabbit:host"]; + var port = int.Parse(Configuration["service:rabbit:port"]); + endpoint = new AmqpTcpEndpoint(host, port); + } + else + { + endpoint = new AmqpTcpEndpoint(new Uri(connectionString)); + } + var factory = new ConnectionFactory() { - HostName = Configuration["service:rabbit:host"], - Port = int.Parse(Configuration["service:rabbit:port"]) + Endpoint = endpoint, }; var connection = factory.CreateConnection(); var channel = connection.CreateModel(); diff --git a/samples/multi-project/backend/backend.csproj b/samples/multi-project/backend/backend.csproj index 8ceaed10..07b43835 100644 --- a/samples/multi-project/backend/backend.csproj +++ b/samples/multi-project/backend/backend.csproj @@ -6,12 +6,8 @@ + - - - - - diff --git a/samples/multi-project/deploy/rabbitmq.yaml b/samples/multi-project/deploy/rabbitmq.yaml new file mode 100644 index 00000000..b5502889 --- /dev/null +++ b/samples/multi-project/deploy/rabbitmq.yaml @@ -0,0 +1,50 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: rabbitmq + labels: + app.kubernetes.io/name: rabbitmq + app.kubernetes.io/part-of: multi-project +spec: + selector: + matchLabels: + app.kubernetes.io/name: rabbitmq + template: + metadata: + labels: + app.kubernetes.io/name: rabbitmq + app.kubernetes.io/part-of: multi-project + spec: + containers: + - name: rabbitmq + image: rabbitmq:3-management + ports: + - containerPort: 5672 + - containerPort: 15672 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: rabbitmq + labels: + app.kubernetes.io/name: rabbitmq + app.kubernetes.io/part-of: multi-project +spec: + selector: + app.kubernetes.io/name: rabbitmq + type: ClusterIP + ports: + - name: rabbitmq + protocol: TCP + port: 5672 + targetPort: 5672 +... +--- +apiVersion: v1 +kind: Secret +metadata: + name: binding-production-rabbit-rabbit-secret +type: Opaque +stringData: + connectionstring: amqp://rabbitmq:5672 \ No newline at end of file diff --git a/samples/multi-project/frontend/IOrderService.cs b/samples/multi-project/frontend/IOrderService.cs new file mode 100644 index 00000000..35dbfe3b --- /dev/null +++ b/samples/multi-project/frontend/IOrderService.cs @@ -0,0 +1,11 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace Shared +{ + [ServiceContract] + public interface IOrderService + { + ValueTask PlaceOrderAsync(Order order); + } +} diff --git a/samples/multi-project/frontend/Order.cs b/samples/multi-project/frontend/Order.cs new file mode 100644 index 00000000..c9ffcc9b --- /dev/null +++ b/samples/multi-project/frontend/Order.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.Serialization; + +namespace Shared +{ + [DataContract] + public class Order + { + [DataMember(Order = 1)] + public Guid OrderId { get; set; } + + [DataMember(Order = 2)] + public string UserId { get; set; } = default!; + + [DataMember(Order = 3)] + public DateTime CreatedTime { get; set; } + } +} diff --git a/samples/multi-project/frontend/Program.cs b/samples/multi-project/frontend/Program.cs index 0e87e1b8..9b644dfb 100644 --- a/samples/multi-project/frontend/Program.cs +++ b/samples/multi-project/frontend/Program.cs @@ -1,5 +1,8 @@ +using System; using System.Diagnostics; +using System.IO; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using ProtoBuf.Grpc.Client; @@ -17,6 +20,14 @@ namespace Frontend public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(config => + { + foreach (var directory in Directory.GetDirectories("/var/tye/bindings/")) + { + Console.WriteLine($"Adding config in '{directory}'."); + config.AddKeyPerFile(directory, optional: true); + } + }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/samples/multi-project/frontend/frontend.csproj b/samples/multi-project/frontend/frontend.csproj index e1b8b40a..4a763c86 100644 --- a/samples/multi-project/frontend/frontend.csproj +++ b/samples/multi-project/frontend/frontend.csproj @@ -5,12 +5,9 @@ + - - - - diff --git a/samples/multi-project/multi-project.sln b/samples/multi-project/multi-project.sln index cd74fd03..3dc1354d 100644 --- a/samples/multi-project/multi-project.sln +++ b/samples/multi-project/multi-project.sln @@ -9,8 +9,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "frontend", "frontend\fronte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "worker", "worker\worker.csproj", "{D96428FD-1ADB-436F-B61B-04C632FBCD34}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "shared", "shared\shared.csproj", "{80DE3A1B-78CC-48F7-B6BE-0C4878666A12}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,17 +58,5 @@ Global {D96428FD-1ADB-436F-B61B-04C632FBCD34}.Release|x64.Build.0 = Release|Any CPU {D96428FD-1ADB-436F-B61B-04C632FBCD34}.Release|x86.ActiveCfg = Release|Any CPU {D96428FD-1ADB-436F-B61B-04C632FBCD34}.Release|x86.Build.0 = Release|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Debug|x64.ActiveCfg = Debug|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Debug|x64.Build.0 = Debug|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Debug|x86.ActiveCfg = Debug|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Debug|x86.Build.0 = Debug|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Release|Any CPU.Build.0 = Release|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Release|x64.ActiveCfg = Release|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Release|x64.Build.0 = Release|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Release|x86.ActiveCfg = Release|Any CPU - {80DE3A1B-78CC-48F7-B6BE-0C4878666A12}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/samples/multi-project/shared/shared.csproj b/samples/multi-project/shared/shared.csproj deleted file mode 100644 index 1d636695..00000000 --- a/samples/multi-project/shared/shared.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - diff --git a/samples/multi-project/tye.yaml b/samples/multi-project/tye.yaml index 975d8545..c855c843 100644 --- a/samples/multi-project/tye.yaml +++ b/samples/multi-project/tye.yaml @@ -1,6 +1,7 @@ # tye application configuration file # read all about it at https://github.com/dotnet/tye name: multi-project +registry: rynowak services: - name: backend project: backend/backend.csproj @@ -18,10 +19,3 @@ services: bindings: - port: 5672 protocol: rabbitmq - - name: management - port: 15672 -- name: zipkin - dockerImage: openzipkin/zipkin - bindings: - - port: 9411 - protocol: http diff --git a/samples/multi-project/worker/Program.cs b/samples/multi-project/worker/Program.cs index 7362d655..256ca676 100644 --- a/samples/multi-project/worker/Program.cs +++ b/samples/multi-project/worker/Program.cs @@ -1,4 +1,7 @@ - +using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.KeyPerFile; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -13,6 +16,17 @@ namespace Worker public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(config => + { + if (Directory.Exists("/var/tye/bindings/")) + { + foreach (var directory in Directory.GetDirectories("/var/tye/bindings/")) + { + Console.WriteLine($"Adding config in '{directory}'."); + config.AddKeyPerFile(directory, optional: true); + } + } + }) .ConfigureServices((hostContext, services) => { services.AddHostedService(); diff --git a/samples/multi-project/worker/QueueWorker.cs b/samples/multi-project/worker/QueueWorker.cs index 8a6a736a..f593164e 100644 --- a/samples/multi-project/worker/QueueWorker.cs +++ b/samples/multi-project/worker/QueueWorker.cs @@ -61,10 +61,22 @@ namespace Worker { try { + AmqpTcpEndpoint endpoint; + var connectionString = _configuration["connectionstring:rabbit"]; + if (connectionString == null) + { + var host = _configuration["service:rabbit:host"]; + var port = int.Parse(_configuration["service:rabbit:port"]); + endpoint = new AmqpTcpEndpoint(host, port); + } + else + { + endpoint = new AmqpTcpEndpoint(new Uri(connectionString)); + } + var factory = new ConnectionFactory() { - HostName = _configuration["service:rabbit:host"], - Port = int.Parse(_configuration["service:rabbit:port"]) + Endpoint = endpoint, }; var connection = factory.CreateConnection(); diff --git a/samples/multi-project/worker/worker.csproj b/samples/multi-project/worker/worker.csproj index 57d31d26..75cb35fb 100644 --- a/samples/multi-project/worker/worker.csproj +++ b/samples/multi-project/worker/worker.csproj @@ -7,6 +7,7 @@ + diff --git a/src/opulence/Opulence/Service.cs b/src/opulence/Opulence/Service.cs index 77d89d45..6805cd8d 100644 --- a/src/opulence/Opulence/Service.cs +++ b/src/opulence/Opulence/Service.cs @@ -19,16 +19,12 @@ namespace Opulence public GeneratedAssets GeneratedAssets { get; } = new GeneratedAssets(); - public int? Port { get; set; } - - public string? Protocol { get; set; } - public Source? Source { get; set; } public Dictionary Environment { get; set; } = new Dictionary(); - public int Replicas { get; set; } = 1; - + // Represents bindings *published* by this service + // See GeneratedAssets for bindings consumed by the service public List Bindings { get; } = new List(); } } diff --git a/src/opulence/Opulence/ServiceBinding.cs b/src/opulence/Opulence/ServiceBinding.cs index a34e1edc..7f77ebf8 100644 --- a/src/opulence/Opulence/ServiceBinding.cs +++ b/src/opulence/Opulence/ServiceBinding.cs @@ -6,30 +6,14 @@ namespace Opulence { public ServiceBinding(string name) { - if (name is null) - { - throw new ArgumentNullException(nameof(name)); - } - Name = name; } - public ServiceBinding(Service service) - { - if (service is null) - { - throw new ArgumentNullException(nameof(service)); - } - - Name = service.Name; - - // We don't copy other properties here because a "hardcoded" value of Protocol/Port is treated differently - // from just binding the name when considering environments. - } - - public string Name { get; set; } + public string Name { get; } + public string? Host { get; set; } public string? Protocol { get; set; } public int? Port { get; set; } - public Secret? ConnectionString { get; set; } + public string? ConnectionString { get; set; } + public Secret? Secret { get; set; } } } diff --git a/src/opulence/dotnet-opulence/CombineStep.cs b/src/opulence/dotnet-opulence/CombineStep.cs index d5097d3d..eec7e597 100644 --- a/src/opulence/dotnet-opulence/CombineStep.cs +++ b/src/opulence/dotnet-opulence/CombineStep.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Opulence { @@ -11,80 +10,64 @@ namespace Opulence public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) { - // Process bindings and turn them into environment variables. - foreach (var binding in service.Service.Bindings) + // No need to do this computation for a non-project since we're not deploying it. + if (!(service.Service.Source is Project)) { - // Try to see if the other project is a service in this application. This can help with - // heuristics if the binding doesn't have all possible info specified. - var other = application.Services.FirstOrDefault(s => s.Service.Name == binding.Name); + return Task.CompletedTask; + } - var key = $"SERVICES__{binding.Name}"; + // Process bindings and turn them into environment variables and secrets. There's + // some duplication with the code in m8s (Application.cs) for populating environments. + // + // service.Service.Bindings is the bindings OUT - this step computes bindings IN. + var bindings = new ComputedBindings(); + service.Outputs.Add(bindings); - // Find the value that needs to go in this env-var (in priority order). - string value; - if (binding.ConnectionString != null) + foreach (var other in application.Services) + { + if (object.ReferenceEquals(service, other)) { - binding.ConnectionString.Name ??= $"binding-{binding.Name}"; - - // It's a secret! We don't use env-vars for this. continue; } - else if (binding.Protocol != null) - { - // This is fully specified as a URL. - value = ResolveUri(binding.Protocol, binding.Name, binding.Port); - } - else if (other?.Service.Source != null && other.AppliesToEnvironment(Environment)) - { - // If we get here it means that the other service is built from source. - // In this case we'll assume that it's http on 80 as a reasonable guess. - value = ResolveUri(other.Service.Protocol ?? "http", binding.Name, other.Service.Port); - } - else if (other?.Service.Source != null) - { - // The other service is built from source, but doesn't apply to this environment. - // This is likely user-error. - throw new CommandException($"Unable to resolve the uri for binding '{binding.Name}'."); - } - else if (other?.Service.Source == null) - { - // The service isn't built from source. - binding.ConnectionString = new Secret() { Name = $"binding-{binding.Name}", }; - // It's a secret! We don't use env-vars for this. - continue; - } - else + foreach (var binding in other.Service.Bindings) { - // Generic catch all case. We don't expect this to get hit because the three blocks - // above cover all possibilities, but the compiler doesn't agree. - throw new CommandException($"Unable to resolve the uri for binding '{binding.Name}'."); - } + // 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) + { + if (!string.IsNullOrEmpty(binding.ConnectionString)) + { + // Special case for connection strings + bindings.Bindings.Add(new EnvironmentVariableInputBinding($"CONNECTIONSTRING__{configName}", binding.ConnectionString)); + } - service.Service.Environment[key] = value; - } + if (!string.IsNullOrEmpty(binding.Protocol)) + { + bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PROTOCOL", binding.Protocol)); + } - return Task.CompletedTask; - } + if (binding.Port != null) + { + bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PORT", binding.Port.Value.ToString())); + } - private static string ResolveUri(string protocol, string name, int? port) - { - if (protocol == "http" && (port == 80 || port == null)) - { - return $"{protocol}://{name}"; - } - else if (protocol == "https" && (port == 443 || port == null)) - { - return $"{protocol}://{name}"; - } - else if (port == null) - { - return $"{protocol}://{name}"; - } - else - { - return $"{protocol}://{name}:{port}"; + bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Service.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", + filename: $"CONNECTIONSTRING__{configName}", + other, + binding)); + } + } } + + return Task.CompletedTask; } } } diff --git a/src/opulence/dotnet-opulence/ComputedBindings.cs b/src/opulence/dotnet-opulence/ComputedBindings.cs new file mode 100644 index 00000000..1fb3138b --- /dev/null +++ b/src/opulence/dotnet-opulence/ComputedBindings.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace Opulence +{ + public class ComputedBindings : ServiceOutput + { + public List Bindings { get; } = new List(); + } + + public abstract class InputBinding + { + } + + public sealed class EnvironmentVariableInputBinding : InputBinding + { + public EnvironmentVariableInputBinding(string name, string value) + { + Name = name; + Value = value; + } + + public string Name { get; } + public string Value { get; } + } + + public sealed class SecretInputBinding : InputBinding + { + public SecretInputBinding(string name, string filename, ServiceEntry service, ServiceBinding binding) + { + Name = name; + Filename = filename; + Service = service; + Binding = binding; + } + + // Used to generate a kubernetes secret + public string Name { get; } + public string? Value { get; } + + // Used to map the secret to a key that ASP.NET Core understandes + public string Filename { get; } + + // Used for informational purposes + public ServiceEntry Service { get; } + public ServiceBinding Binding { get; } + } +} diff --git a/src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs b/src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs index a51021ca..f7a4ac89 100644 --- a/src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs +++ b/src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs @@ -22,7 +22,12 @@ namespace Opulence } service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, service)); - service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, service)); + + if (service.Service.Bindings.Count > 0) + { + service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, service)); + } + return Task.CompletedTask; } } diff --git a/src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs b/src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs index 18cac5c8..803652a6 100644 --- a/src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs +++ b/src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using YamlDotNet.Core; using YamlDotNet.RepresentationModel; namespace Opulence @@ -49,13 +50,17 @@ namespace Opulence var ports = new YamlSequenceNode(); spec.Add("ports", ports); - var port = new YamlMappingNode(); - ports.Add(port); + // We figure out the port based on bindings + foreach (var binding in service.Service.Bindings) + { + var port = new YamlMappingNode(); + ports.Add(port); - port.Add("name", "web"); - port.Add("protocol", "TCP"); - port.Add("port", "80"); - port.Add("targetPort", "80"); + port.Add("name", binding.Name ?? "web"); + port.Add("protocol", "TCP"); // we use assume TCP. YOLO + port.Add("port", binding.Port?.ToString() ?? "80"); + port.Add("targetPort", binding.Port?.ToString() ?? "80"); + } return new KubernetesServiceOutput(service.Service.Name, new YamlDocument(root)); } @@ -77,6 +82,8 @@ namespace Opulence throw new ArgumentNullException(nameof(service)); } + var bindings = service.Outputs.OfType().FirstOrDefault(); + var root = new YamlMappingNode(); root.Add("kind", "Deployment"); @@ -126,7 +133,13 @@ namespace Opulence container.Add("name", service.Service.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); - if (service.Service.Environment.Count > 0) + if (service.Service.Environment.Count > 0 || + + // We generate ASPNETCORE_URLS if there are bindings for http + service.Service.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())) { var env = new YamlSequenceNode(); container.Add("env", env); @@ -136,57 +149,88 @@ namespace Opulence env.Add(new YamlMappingNode() { { "name", kvp.Key }, - { "value", kvp.Value.ToString() }, + { "value", new YamlScalarNode(kvp.Value.ToString()) { Style = ScalarStyle.SingleQuoted, } }, }); } + + foreach (var binding in service.Service.Bindings) + { + if (binding.Protocol == "http" || binding.Protocol == null) + { + var port = binding.Port ?? 80; + env.Add(new YamlMappingNode() + { + { "name", "ASPNETCORE_URLS" }, + { "value", $"http://*{(binding.Port == 80 ? "" : (":" + binding.Port.ToString()))}" }, + }); + } + } + + if (bindings is object) + { + foreach (var binding in bindings.Bindings.OfType()) + { + env.Add(new YamlMappingNode() + { + { "name", binding.Name }, + { "value", new YamlScalarNode(binding.Value) { Style = ScalarStyle.SingleQuoted, } }, + }); + } + } } - if (service.Service.Bindings.Any(b => b.ConnectionString != null)) + if (bindings is object && bindings.Bindings.OfType().Any()) { var volumeMounts = new YamlSequenceNode(); container.Add("volumeMounts", volumeMounts); - foreach (var binding in service.Service.Bindings.Where(b => b.ConnectionString != null)) + foreach (var binding in bindings.Bindings.OfType()) { var volumeMount = new YamlMappingNode(); volumeMounts.Add(volumeMount); - volumeMount.Add("name", $"{binding.Name}-secret"); - volumeMount.Add("mountPath", $"/var/bindings/{binding.Name}"); + 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("readOnly", "true"); } } - var ports = new YamlSequenceNode(); - container.Add("ports", ports); + if (service.Service.Bindings.Count > 0) + { + var ports = new YamlSequenceNode(); + container.Add("ports", ports); - var containerPort = new YamlMappingNode(); - ports.Add(containerPort); - containerPort.Add("containerPort", "80"); + foreach (var binding in service.Service.Bindings) + { + var containerPort = new YamlMappingNode(); + ports.Add(containerPort); + containerPort.Add("containerPort", binding.Port?.ToString() ?? "80"); + } + } } - if (service.Service.Bindings.Any(b => b.ConnectionString != null)) + if (bindings.Bindings.OfType().Any()) { var volumes = new YamlSequenceNode(); spec.Add("volumes", volumes); - foreach (var binding in service.Service.Bindings.Where(b => b.ConnectionString != null)) + foreach (var binding in bindings.Bindings.OfType()) { var volume = new YamlMappingNode(); volumes.Add(volume); - volume.Add("name", $"{binding.Name}-secret"); + volume.Add("name", $"{binding.Service.Service.Name}-{binding.Binding.Name}"); var secret = new YamlMappingNode(); volume.Add("secret", secret); - secret.Add("secretName", binding.ConnectionString!.Name!); + secret.Add("secretName", binding.Name!); var items = new YamlSequenceNode(); secret.Add("items", items); var item = new YamlMappingNode(); items.Add(item); - item.Add("key", "uri"); - item.Add("path", $"SERVICES__{binding.Name}"); + item.Add("key", "connectionstring"); + item.Add("path", binding.Filename); } } diff --git a/src/opulence/dotnet-opulence/Program.cs b/src/opulence/dotnet-opulence/Program.cs index c2ad6668..7f1d6234 100644 --- a/src/opulence/dotnet-opulence/Program.cs +++ b/src/opulence/dotnet-opulence/Program.cs @@ -1,4 +1,4 @@ -namespace Opulence +namespace Opulence { static class Program { @@ -6,4 +6,4 @@ namespace Opulence { } } -} \ No newline at end of file +} diff --git a/src/tye/ConfigModel/ConfigServiceBinding.cs b/src/tye/ConfigModel/ConfigServiceBinding.cs index d8cd793d..ed631582 100644 --- a/src/tye/ConfigModel/ConfigServiceBinding.cs +++ b/src/tye/ConfigModel/ConfigServiceBinding.cs @@ -4,8 +4,7 @@ namespace Tye.ConfigModel { internal class ConfigServiceBinding { - [Required] - public string Name { get; set; } = default!; + public string? Name { get; set; } public string? ConnectionString { get; set; } public int? Port { get; set; } public int? InternalPort { get; set; } diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index 5525a176..978a2622 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -30,13 +30,35 @@ namespace Tye } var application = ConfigFactory.FromFile(path); - return ExecuteAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); + return ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); }); return command; } - private static async Task ExecuteAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) + private static async Task ExecuteDeployAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) + { + var opulenceApplication = await CreateOpulenceApplicationAsync(output, application, interactive); + var steps = new List() + { + new CombineStep() { Environment = environment, }, + new BuildDockerImageStep() { Environment = environment, }, + new PushDockerImageStep() { Environment = environment, }, + }; + + steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, }); + steps.Add(new DeployServiceYamlStep() { Environment = environment, }); + + var executor = new ServiceExecutor(output, opulenceApplication, steps); + foreach (var service in opulenceApplication.Services) + { + await executor.ExecuteAsync(service); + } + + await DeployApplicationManifestAsync(output, opulenceApplication, application.Source.Directory.Name, environment); + } + + private static async Task CreateOpulenceApplicationAsync(OutputContext output, ConfigApplication application, bool interactive) { var globals = new ApplicationGlobals() { @@ -54,14 +76,48 @@ namespace Tye { Source = project, }; + + 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); await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(projectFile), project); - var container = new ContainerInfo(); + var container = new ContainerInfo() + { + // Single-phase workflow doesn't currently work. + UseMultiphaseDockerfile = true, + }; 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 opulenceApplication = new OpulenceApplicationAdapter(application, globals, services); @@ -83,23 +139,7 @@ namespace Tye } } - var steps = new List() - { - new CombineStep() { Environment = environment, }, - new BuildDockerImageStep() { Environment = environment, }, - new PushDockerImageStep() { Environment = environment, }, - }; - - steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, }); - steps.Add(new DeployServiceYamlStep() { Environment = environment, }); - - var executor = new ServiceExecutor(output, opulenceApplication, steps); - foreach (var service in opulenceApplication.Services) - { - await executor.ExecuteAsync(service); - } - - await DeployApplicationManifestAsync(output, opulenceApplication, application.Source.Directory.Name, environment); + return opulenceApplication; } private static async Task DeployApplicationManifestAsync(OutputContext output, Opulence.Application application, string applicationName, string environment) diff --git a/src/tye/Program.GenerateCommand.cs b/src/tye/Program.GenerateCommand.cs index 27696f94..2bc399f8 100644 --- a/src/tye/Program.GenerateCommand.cs +++ b/src/tye/Program.GenerateCommand.cs @@ -17,6 +17,7 @@ namespace Tye var command = new Command("generate", "Generate kubernetes manifests") { CommonArguments.Path_Required, + StandardOptions.Interactive, StandardOptions.Verbosity, }; @@ -24,7 +25,7 @@ namespace Tye // not documenting it right now. command.IsHidden = true; - command.Handler = CommandHandler.Create((console, path, verbosity) => + command.Handler = CommandHandler.Create((console, path, verbosity, interactive) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) @@ -33,59 +34,15 @@ namespace Tye } var application = ConfigFactory.FromFile(path); - return ExecuteAsync(new OutputContext(console, verbosity), application, environment: "production"); + return ExecuteGenerateAsync(new OutputContext(console, verbosity), application, environment: "production", interactive); }); return command; } - private static async Task ExecuteAsync(OutputContext output, ConfigApplication application, string environment) + private static async Task ExecuteGenerateAsync(OutputContext output, ConfigApplication application, string environment, bool interactive) { - 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 project = new Project(projectFile); - var service = new Service(configService.Name) - { - Source = project, - }; - var serviceEntry = new ServiceEntry(service, configService.Name); - - await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(projectFile), project); - - var container = new ContainerInfo(); - service.GeneratedAssets.Container = container; - services.Add(serviceEntry); - } - } - - var opulenceApplication = new OpulenceApplicationAdapter(application, globals, services); - if (opulenceApplication.Globals.Registry?.Hostname == null ) - { - var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)"); - opulenceApplication.Globals.Registry = new ContainerRegistry(registry); - } - else if (opulenceApplication.Globals.Registry?.Hostname == null) - { - throw new CommandException("A registry is required for generate operations. Add the registry to 'tye.yaml' or use '-i' for interactive mode."); - } - - foreach (var service in opulenceApplication.Services) - { - if (service.Service.Source is Project project && service.Service.GeneratedAssets.Container is ContainerInfo container) - { - DockerfileGenerator.ApplyContainerDefaults(opulenceApplication, service, project, container); - } - } - + var opulenceApplication = await CreateOpulenceApplicationAsync(output, application, interactive); var steps = new List() { new CombineStep() { Environment = environment, },