Browse Source

Implement binding support for deploy

pull/36/head
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.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<Startup>()

17
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();

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

@ -6,12 +6,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="3.1.1" />
<PackageReference Include="RabbitMQ.Client" Version="5.1.2" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.21" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared\shared.csproj" />
</ItemGroup>
</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.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<Startup>();

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

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

14
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

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
# 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

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.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<QueueWorker>();

16
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();

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

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

8
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<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>();
}
}

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

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

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.CreateService(output, application, service));
if (service.Service.Bindings.Count > 0)
{
service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, service));
}
return Task.CompletedTask;
}
}

90
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<ComputedBindings>().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<EnvironmentVariableInputBinding>().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<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();
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();
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<SecretInputBinding>().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<SecretInputBinding>())
{
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);
}
}

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

@ -1,4 +1,4 @@
namespace Opulence
namespace Opulence
{
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
{
[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; }

80
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<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()
{
@ -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<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);
return opulenceApplication;
}
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")
{
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<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
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<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 opulenceApplication = await CreateOpulenceApplicationAsync(output, application, interactive);
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },

Loading…
Cancel
Save