Browse Source

Implement binding support for deploy

davidfowl/dependencies
Ryan Nowak 7 years ago
parent
commit
2201298211
  1. 0
      samples/multi-project/backend/IOrderService.cs
  2. 0
      samples/multi-project/backend/Order.cs
  3. 11
      samples/multi-project/backend/Program.cs
  4. 17
      samples/multi-project/backend/Startup.cs
  5. 6
      samples/multi-project/backend/backend.csproj
  6. 50
      samples/multi-project/deploy/rabbitmq.yaml
  7. 11
      samples/multi-project/frontend/IOrderService.cs
  8. 18
      samples/multi-project/frontend/Order.cs
  9. 11
      samples/multi-project/frontend/Program.cs
  10. 5
      samples/multi-project/frontend/frontend.csproj
  11. 14
      samples/multi-project/multi-project.sln
  12. 12
      samples/multi-project/shared/shared.csproj
  13. 8
      samples/multi-project/tye.yaml
  14. 16
      samples/multi-project/worker/Program.cs
  15. 16
      samples/multi-project/worker/QueueWorker.cs
  16. 1
      samples/multi-project/worker/worker.csproj
  17. 8
      src/opulence/Opulence/Service.cs
  18. 24
      src/opulence/Opulence/ServiceBinding.cs
  19. 111
      src/opulence/dotnet-opulence/CombineStep.cs
  20. 47
      src/opulence/dotnet-opulence/ComputedBindings.cs
  21. 7
      src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs
  22. 90
      src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs
  23. 4
      src/opulence/dotnet-opulence/Program.cs
  24. 3
      src/tye/ConfigModel/ConfigServiceBinding.cs
  25. 80
      src/tye/Program.DeployCommand.cs
  26. 53
      src/tye/Program.GenerateCommand.cs

0
samples/multi-project/shared/IOrderService.cs → samples/multi-project/backend/IOrderService.cs

0
samples/multi-project/shared/Order.cs → samples/multi-project/backend/Order.cs

11
samples/multi-project/backend/Program.cs

@ -1,6 +1,9 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
namespace Backend namespace Backend
@ -16,6 +19,14 @@ namespace Backend
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(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 => .ConfigureWebHostDefaults(web =>
{ {
web.UseStartup<Startup>() web.UseStartup<Startup>()

17
samples/multi-project/backend/Startup.cs

@ -1,3 +1,4 @@
using System;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -25,10 +26,22 @@ namespace Backend
services.AddSingleton(sp => 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() var factory = new ConnectionFactory()
{ {
HostName = Configuration["service:rabbit:host"], Endpoint = endpoint,
Port = int.Parse(Configuration["service:rabbit:port"])
}; };
var connection = factory.CreateConnection(); var connection = factory.CreateConnection();
var channel = connection.CreateModel(); var channel = connection.CreateModel();

6
samples/multi-project/backend/backend.csproj

@ -6,12 +6,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="3.1.1" />
<PackageReference Include="RabbitMQ.Client" Version="5.1.2" /> <PackageReference Include="RabbitMQ.Client" Version="5.1.2" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.21" /> <PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.21" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared\shared.csproj" />
</ItemGroup>
</Project> </Project>

50
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

11
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);
}
}

18
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; }
}
}

11
samples/multi-project/frontend/Program.cs

@ -1,5 +1,8 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using ProtoBuf.Grpc.Client; using ProtoBuf.Grpc.Client;
@ -17,6 +20,14 @@ namespace Frontend
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(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 => .ConfigureWebHostDefaults(webBuilder =>
{ {
webBuilder.UseStartup<Startup>(); webBuilder.UseStartup<Startup>();

5
samples/multi-project/frontend/frontend.csproj

@ -5,12 +5,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="3.1.1" />
<PackageReference Include="Grpc.Net.Client" Version="2.26.0" /> <PackageReference Include="Grpc.Net.Client" Version="2.26.0" />
<PackageReference Include="protobuf-net.Grpc" Version="1.0.21" /> <PackageReference Include="protobuf-net.Grpc" Version="1.0.21" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared\shared.csproj" />
</ItemGroup>
</Project> </Project>

14
samples/multi-project/multi-project.sln

@ -9,8 +9,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "frontend", "frontend\fronte
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "worker", "worker\worker.csproj", "{D96428FD-1ADB-436F-B61B-04C632FBCD34}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "worker", "worker\worker.csproj", "{D96428FD-1ADB-436F-B61B-04C632FBCD34}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "shared", "shared\shared.csproj", "{80DE3A1B-78CC-48F7-B6BE-0C4878666A12}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|x64.Build.0 = Release|Any CPU
{D96428FD-1ADB-436F-B61B-04C632FBCD34}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
EndGlobal EndGlobal

12
samples/multi-project/shared/shared.csproj

@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.ServiceModel.Primitives" Version="4.6.0" />
<PackageReference Include="protobuf-net.Grpc" Version="1.0.21" />
</ItemGroup>
</Project>

8
samples/multi-project/tye.yaml

@ -1,6 +1,7 @@
# tye application configuration file # tye application configuration file
# read all about it at https://github.com/dotnet/tye # read all about it at https://github.com/dotnet/tye
name: multi-project name: multi-project
registry: rynowak
services: services:
- name: backend - name: backend
project: backend/backend.csproj project: backend/backend.csproj
@ -18,10 +19,3 @@ services:
bindings: bindings:
- port: 5672 - port: 5672
protocol: rabbitmq protocol: rabbitmq
- name: management
port: 15672
- name: zipkin
dockerImage: openzipkin/zipkin
bindings:
- port: 9411
protocol: http

16
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.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@ -13,6 +16,17 @@ namespace Worker
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(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) => .ConfigureServices((hostContext, services) =>
{ {
services.AddHostedService<QueueWorker>(); services.AddHostedService<QueueWorker>();

16
samples/multi-project/worker/QueueWorker.cs

@ -61,10 +61,22 @@ namespace Worker
{ {
try 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() var factory = new ConnectionFactory()
{ {
HostName = _configuration["service:rabbit:host"], Endpoint = endpoint,
Port = int.Parse(_configuration["service:rabbit:port"])
}; };
var connection = factory.CreateConnection(); var connection = factory.CreateConnection();

1
samples/multi-project/worker/worker.csproj

@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.1" />
<PackageReference Include="RabbitMQ.Client" Version="5.1.2" /> <PackageReference Include="RabbitMQ.Client" Version="5.1.2" />
</ItemGroup> </ItemGroup>

8
src/opulence/Opulence/Service.cs

@ -19,16 +19,12 @@ namespace Opulence
public GeneratedAssets GeneratedAssets { get; } = new GeneratedAssets(); public GeneratedAssets GeneratedAssets { get; } = new GeneratedAssets();
public int? Port { get; set; }
public string? Protocol { get; set; }
public Source? Source { get; set; } public Source? Source { get; set; }
public Dictionary<string, object> Environment { get; set; } = new Dictionary<string, object>(); public Dictionary<string, object> Environment { get; set; } = new Dictionary<string, object>();
public int Replicas { get; set; } = 1; // Represents bindings *published* by this service
// See GeneratedAssets for bindings consumed by the service
public List<ServiceBinding> Bindings { get; } = new List<ServiceBinding>(); public List<ServiceBinding> Bindings { get; } = new List<ServiceBinding>();
} }
} }

24
src/opulence/Opulence/ServiceBinding.cs

@ -6,30 +6,14 @@ namespace Opulence
{ {
public ServiceBinding(string name) public ServiceBinding(string name)
{ {
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name; Name = name;
} }
public ServiceBinding(Service service) public string Name { get; }
{ public string? Host { get; set; }
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? Protocol { get; set; } public string? Protocol { get; set; }
public int? Port { get; set; } public int? Port { get; set; }
public Secret? ConnectionString { get; set; } public string? ConnectionString { get; set; }
public Secret? Secret { get; set; }
} }
} }

111
src/opulence/dotnet-opulence/CombineStep.cs

@ -1,5 +1,4 @@
using System.Linq; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Opulence namespace Opulence
{ {
@ -11,80 +10,64 @@ namespace Opulence
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{ {
// Process bindings and turn them into environment variables. // No need to do this computation for a non-project since we're not deploying it.
foreach (var binding in service.Service.Bindings) if (!(service.Service.Source is Project))
{ {
// Try to see if the other project is a service in this application. This can help with return Task.CompletedTask;
// heuristics if the binding doesn't have all possible info specified. }
var other = application.Services.FirstOrDefault(s => s.Service.Name == binding.Name);
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). foreach (var other in application.Services)
string value; {
if (binding.ConnectionString != null) if (object.ReferenceEquals(service, other))
{ {
binding.ConnectionString.Name ??= $"binding-{binding.Name}";
// It's a secret! We don't use env-vars for this.
continue; 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. foreach (var binding in other.Service.Bindings)
continue;
}
else
{ {
// Generic catch all case. We don't expect this to get hit because the three blocks // The other thing is a project, and will be deployed along with this
// above cover all possibilities, but the compiler doesn't agree. // service.
throw new CommandException($"Unable to resolve the uri for binding '{binding.Name}'."); 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) bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Service.Name));
{ }
if (protocol == "http" && (port == 80 || port == null)) else
{ {
return $"{protocol}://{name}"; // The other service is not a project, so we'll use secrets.
} bindings.Bindings.Add(new SecretInputBinding(
else if (protocol == "https" && (port == 443 || port == null)) name: $"binding-{Environment}-{other.Service.Name}-{binding.Name}-secret",
{ filename: $"CONNECTIONSTRING__{configName}",
return $"{protocol}://{name}"; other,
} binding));
else if (port == null) }
{ }
return $"{protocol}://{name}";
}
else
{
return $"{protocol}://{name}:{port}";
} }
return Task.CompletedTask;
} }
} }
} }

47
src/opulence/dotnet-opulence/ComputedBindings.cs

@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace Opulence
{
public class ComputedBindings : ServiceOutput
{
public List<InputBinding> Bindings { get; } = new List<InputBinding>();
}
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; }
}
}

7
src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs

@ -22,7 +22,12 @@ namespace Opulence
} }
service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, service)); 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; return Task.CompletedTask;
} }
} }

90
src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel; using YamlDotNet.RepresentationModel;
namespace Opulence namespace Opulence
@ -49,13 +50,17 @@ namespace Opulence
var ports = new YamlSequenceNode(); var ports = new YamlSequenceNode();
spec.Add("ports", ports); spec.Add("ports", ports);
var port = new YamlMappingNode(); // We figure out the port based on bindings
ports.Add(port); foreach (var binding in service.Service.Bindings)
{
var port = new YamlMappingNode();
ports.Add(port);
port.Add("name", "web"); port.Add("name", binding.Name ?? "web");
port.Add("protocol", "TCP"); port.Add("protocol", "TCP"); // we use assume TCP. YOLO
port.Add("port", "80"); port.Add("port", binding.Port?.ToString() ?? "80");
port.Add("targetPort", "80"); port.Add("targetPort", binding.Port?.ToString() ?? "80");
}
return new KubernetesServiceOutput(service.Service.Name, new YamlDocument(root)); return new KubernetesServiceOutput(service.Service.Name, new YamlDocument(root));
} }
@ -77,6 +82,8 @@ namespace Opulence
throw new ArgumentNullException(nameof(service)); throw new ArgumentNullException(nameof(service));
} }
var bindings = service.Outputs.OfType<ComputedBindings>().FirstOrDefault();
var root = new YamlMappingNode(); var root = new YamlMappingNode();
root.Add("kind", "Deployment"); 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("name", service.Service.Name); // NOTE: to really support multiple images we'd need to generate unique names.
container.Add("image", $"{image.ImageName}:{image.ImageTag}"); 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<EnvironmentVariableInputBinding>().Any()))
{ {
var env = new YamlSequenceNode(); var env = new YamlSequenceNode();
container.Add("env", env); container.Add("env", env);
@ -136,57 +149,88 @@ namespace Opulence
env.Add(new YamlMappingNode() env.Add(new YamlMappingNode()
{ {
{ "name", kvp.Key }, { "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<EnvironmentVariableInputBinding>())
{
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<SecretInputBinding>().Any())
{ {
var volumeMounts = new YamlSequenceNode(); var volumeMounts = new YamlSequenceNode();
container.Add("volumeMounts", volumeMounts); container.Add("volumeMounts", volumeMounts);
foreach (var binding in service.Service.Bindings.Where(b => b.ConnectionString != null)) foreach (var binding in bindings.Bindings.OfType<SecretInputBinding>())
{ {
var volumeMount = new YamlMappingNode(); var volumeMount = new YamlMappingNode();
volumeMounts.Add(volumeMount); volumeMounts.Add(volumeMount);
volumeMount.Add("name", $"{binding.Name}-secret"); volumeMount.Add("name", $"{binding.Service.Service.Name}-{binding.Binding.Name}");
volumeMount.Add("mountPath", $"/var/bindings/{binding.Name}"); volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Service.Name}-{binding.Binding.Name}");
volumeMount.Add("readOnly", "true"); volumeMount.Add("readOnly", "true");
} }
} }
var ports = new YamlSequenceNode(); if (service.Service.Bindings.Count > 0)
container.Add("ports", ports); {
var ports = new YamlSequenceNode();
container.Add("ports", ports);
var containerPort = new YamlMappingNode(); foreach (var binding in service.Service.Bindings)
ports.Add(containerPort); {
containerPort.Add("containerPort", "80"); 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<SecretInputBinding>().Any())
{ {
var volumes = new YamlSequenceNode(); var volumes = new YamlSequenceNode();
spec.Add("volumes", volumes); spec.Add("volumes", volumes);
foreach (var binding in service.Service.Bindings.Where(b => b.ConnectionString != null)) foreach (var binding in bindings.Bindings.OfType<SecretInputBinding>())
{ {
var volume = new YamlMappingNode(); var volume = new YamlMappingNode();
volumes.Add(volume); volumes.Add(volume);
volume.Add("name", $"{binding.Name}-secret"); volume.Add("name", $"{binding.Service.Service.Name}-{binding.Binding.Name}");
var secret = new YamlMappingNode(); var secret = new YamlMappingNode();
volume.Add("secret", secret); volume.Add("secret", secret);
secret.Add("secretName", binding.ConnectionString!.Name!); secret.Add("secretName", binding.Name!);
var items = new YamlSequenceNode(); var items = new YamlSequenceNode();
secret.Add("items", items); secret.Add("items", items);
var item = new YamlMappingNode(); var item = new YamlMappingNode();
items.Add(item); items.Add(item);
item.Add("key", "uri"); item.Add("key", "connectionstring");
item.Add("path", $"SERVICES__{binding.Name}"); item.Add("path", binding.Filename);
} }
} }

4
src/opulence/dotnet-opulence/Program.cs

@ -1,4 +1,4 @@
namespace Opulence namespace Opulence
{ {
static class Program static class Program
{ {
@ -6,4 +6,4 @@ namespace Opulence
{ {
} }
} }
} }

3
src/tye/ConfigModel/ConfigServiceBinding.cs

@ -4,8 +4,7 @@ namespace Tye.ConfigModel
{ {
internal class ConfigServiceBinding internal class ConfigServiceBinding
{ {
[Required] public string? Name { get; set; }
public string Name { get; set; } = default!;
public string? ConnectionString { get; set; } public string? ConnectionString { get; set; }
public int? Port { get; set; } public int? Port { get; set; }
public int? InternalPort { get; set; } public int? InternalPort { get; set; }

80
src/tye/Program.DeployCommand.cs

@ -30,13 +30,35 @@ namespace Tye
} }
var application = ConfigFactory.FromFile(path); 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; 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<ServiceExecutor.Step>()
{
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<OpulenceApplicationAdapter> CreateOpulenceApplicationAsync(OutputContext output, ConfigApplication application, bool interactive)
{ {
var globals = new ApplicationGlobals() var globals = new ApplicationGlobals()
{ {
@ -54,14 +76,48 @@ namespace Tye
{ {
Source = project, 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); var serviceEntry = new ServiceEntry(service, configService.Name);
await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(projectFile), project); 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; service.GeneratedAssets.Container = container;
services.Add(serviceEntry); 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); var opulenceApplication = new OpulenceApplicationAdapter(application, globals, services);
@ -83,23 +139,7 @@ namespace Tye
} }
} }
var steps = new List<ServiceExecutor.Step>() return opulenceApplication;
{
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 DeployApplicationManifestAsync(OutputContext output, Opulence.Application application, string applicationName, string environment) private static async Task DeployApplicationManifestAsync(OutputContext output, Opulence.Application application, string applicationName, string environment)

53
src/tye/Program.GenerateCommand.cs

@ -17,6 +17,7 @@ namespace Tye
var command = new Command("generate", "Generate kubernetes manifests") var command = new Command("generate", "Generate kubernetes manifests")
{ {
CommonArguments.Path_Required, CommonArguments.Path_Required,
StandardOptions.Interactive,
StandardOptions.Verbosity, StandardOptions.Verbosity,
}; };
@ -24,7 +25,7 @@ namespace Tye
// not documenting it right now. // not documenting it right now.
command.IsHidden = true; command.IsHidden = true;
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity>((console, path, verbosity) => command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool>((console, path, verbosity, interactive) =>
{ {
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null) if (path is null)
@ -33,59 +34,15 @@ namespace Tye
} }
var application = ConfigFactory.FromFile(path); 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; 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() var opulenceApplication = await CreateOpulenceApplicationAsync(output, application, interactive);
{
Name = application.Name,
Registry = application.Registry is null ? null : new ContainerRegistry(application.Registry),
};
var services = new List<Opulence.ServiceEntry>();
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 steps = new List<ServiceExecutor.Step>() var steps = new List<ServiceExecutor.Step>()
{ {
new CombineStep() { Environment = environment, }, new CombineStep() { Environment = environment, },

Loading…
Cancel
Save