diff --git a/samples/nginx-ingress/ApplicationA/ApplicationA.csproj b/samples/nginx-ingress/ApplicationA/ApplicationA.csproj
new file mode 100644
index 00000000..92605c5a
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationA/ApplicationA.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netcoreapp3.1
+
+
+
diff --git a/samples/nginx-ingress/ApplicationA/Program.cs b/samples/nginx-ingress/ApplicationA/Program.cs
new file mode 100644
index 00000000..56492c75
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationA/Program.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ApplicationA
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationA/Properties/launchSettings.json b/samples/nginx-ingress/ApplicationA/Properties/launchSettings.json
new file mode 100644
index 00000000..767a1d0b
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationA/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:2755",
+ "sslPort": 44369
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "ApplicationA": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationA/Startup.cs b/samples/nginx-ingress/ApplicationA/Startup.cs
new file mode 100644
index 00000000..257fd799
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationA/Startup.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace ApplicationA
+{
+ public class Startup
+ {
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapGet("/", async context =>
+ {
+ await context.Response.WriteAsync("Hello from Application A " + Environment.GetEnvironmentVariable("APP_INSTANCE") ?? Environment.GetEnvironmentVariable("HOSTNAME"));
+ });
+ });
+ }
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationA/appsettings.Development.json b/samples/nginx-ingress/ApplicationA/appsettings.Development.json
new file mode 100644
index 00000000..8983e0fc
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationA/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationA/appsettings.json b/samples/nginx-ingress/ApplicationA/appsettings.json
new file mode 100644
index 00000000..d9d9a9bf
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationA/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/nginx-ingress/ApplicationB/ApplicationB.csproj b/samples/nginx-ingress/ApplicationB/ApplicationB.csproj
new file mode 100644
index 00000000..92605c5a
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationB/ApplicationB.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netcoreapp3.1
+
+
+
diff --git a/samples/nginx-ingress/ApplicationB/Program.cs b/samples/nginx-ingress/ApplicationB/Program.cs
new file mode 100644
index 00000000..8b3c38af
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationB/Program.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ApplicationB
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationB/Properties/launchSettings.json b/samples/nginx-ingress/ApplicationB/Properties/launchSettings.json
new file mode 100644
index 00000000..2b1eba0f
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationB/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:19251",
+ "sslPort": 44343
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "ApplicationB": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationB/Startup.cs b/samples/nginx-ingress/ApplicationB/Startup.cs
new file mode 100644
index 00000000..1a6b68d8
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationB/Startup.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace ApplicationB
+{
+ public class Startup
+ {
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapGet("/", async context =>
+ {
+ await context.Response.WriteAsync("Hello from Application B " + Environment.GetEnvironmentVariable("APP_INSTANCE") ?? Environment.GetEnvironmentVariable("HOSTNAME"));
+ });
+ });
+ }
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationB/appsettings.Development.json b/samples/nginx-ingress/ApplicationB/appsettings.Development.json
new file mode 100644
index 00000000..8983e0fc
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationB/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/samples/nginx-ingress/ApplicationB/appsettings.json b/samples/nginx-ingress/ApplicationB/appsettings.json
new file mode 100644
index 00000000..d9d9a9bf
--- /dev/null
+++ b/samples/nginx-ingress/ApplicationB/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/nginx-ingress/nginx-with-ingress.sln b/samples/nginx-ingress/nginx-with-ingress.sln
new file mode 100644
index 00000000..3dcc942e
--- /dev/null
+++ b/samples/nginx-ingress/nginx-with-ingress.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26124.0
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationA", "ApplicationA\ApplicationA.csproj", "{5A9DC239-55BB-4951-B081-35931BF8C867}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationB", "ApplicationB\ApplicationB.csproj", "{AE1F10D3-BFAE-4D23-ADCF-06770237285D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x64.Build.0 = Debug|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x86.Build.0 = Debug|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x64.ActiveCfg = Release|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x64.Build.0 = Release|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x86.ActiveCfg = Release|Any CPU
+ {5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x86.Build.0 = Release|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x64.Build.0 = Debug|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x86.Build.0 = Debug|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x64.ActiveCfg = Release|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x64.Build.0 = Release|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/samples/nginx-ingress/nginx.conf b/samples/nginx-ingress/nginx.conf
new file mode 100644
index 00000000..35f8f2e0
--- /dev/null
+++ b/samples/nginx-ingress/nginx.conf
@@ -0,0 +1,25 @@
+server {
+ listen 80;
+
+ location /A {
+ proxy_pass http://appA/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection keep-alive;
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /B {
+ proxy_pass http://appB/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection keep-alive;
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
\ No newline at end of file
diff --git a/samples/nginx-ingress/tye.yaml b/samples/nginx-ingress/tye.yaml
new file mode 100644
index 00000000..41d290c1
--- /dev/null
+++ b/samples/nginx-ingress/tye.yaml
@@ -0,0 +1,16 @@
+services:
+- name: nginx
+ image: nginx
+ bindings:
+ - protocol: http
+ volumes:
+ - source: nginx.conf
+ target: /etc/nginx/conf.d/default.conf
+- name: appA
+ project: ApplicationA/ApplicationA.csproj
+ bindings:
+ replicas: 2
+- name: appB
+ project: ApplicationB/ApplicationB.csproj
+ bindings:
+ replicas: 2
diff --git a/src/Microsoft.Tye.Hosting/AggregateApplicationProcessor.cs b/src/Microsoft.Tye.Hosting/AggregateApplicationProcessor.cs
index 900aa069..cd936d83 100644
--- a/src/Microsoft.Tye.Hosting/AggregateApplicationProcessor.cs
+++ b/src/Microsoft.Tye.Hosting/AggregateApplicationProcessor.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.Tye.Hosting.Model;
@@ -28,10 +29,28 @@ namespace Microsoft.Tye.Hosting
public async Task StopAsync(Application application)
{
+ var exceptions = new List();
+
// Shutdown in the opposite order
foreach (var processor in _applicationProcessors.Reverse())
{
- await processor.StopAsync(application);
+ try
+ {
+ await processor.StopAsync(application);
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
+ }
+
+ if (exceptions.Count == 1)
+ {
+ ExceptionDispatchInfo.Throw(exceptions[0]);
+ }
+ else if (exceptions.Count > 0)
+ {
+ throw new AggregateException(exceptions);
}
}
}
diff --git a/src/Microsoft.Tye.Hosting/DockerRunner.cs b/src/Microsoft.Tye.Hosting/DockerRunner.cs
index 021676f4..3807226f 100644
--- a/src/Microsoft.Tye.Hosting/DockerRunner.cs
+++ b/src/Microsoft.Tye.Hosting/DockerRunner.cs
@@ -49,6 +49,49 @@ namespace Microsoft.Tye.Hosting
return;
}
+ var proxies = new List();
+ foreach (var service in application.Services.Values)
+ {
+ if (service.Description.RunInfo is DockerRunInfo || service.Description.Bindings.Count == 0)
+ {
+ continue;
+ }
+
+ // Inject a proxy per non-container service. This allows the container to use normal host names within the
+ // container network to talk to services on the host
+ var proxyContanier = new DockerRunInfo($"mcr.microsoft.com/dotnet/core/sdk:3.1", "dotnet Microsoft.Tye.Proxy.dll")
+ {
+ WorkingDirectory = "/app",
+ NetworkAlias = service.Description.Name,
+ Private = true
+ };
+ var proxyLocation = Path.GetDirectoryName(typeof(Microsoft.Tye.Proxy.Program).Assembly.Location);
+ proxyContanier.VolumeMappings.Add(new DockerVolume(proxyLocation, name: null, target: "/app"));
+ var proxyDescription = new ServiceDescription($"{service.Description.Name}-proxy", proxyContanier);
+ foreach (var binding in service.Description.Bindings)
+ {
+ if (binding.Port == null)
+ {
+ continue;
+ }
+
+ var b = new ServiceBinding()
+ {
+ ConnectionString = binding.ConnectionString,
+ Host = binding.Host,
+ ContainerPort = binding.ContainerPort,
+ Name = binding.Name,
+ Port = binding.Port,
+ Protocol = binding.Protocol
+ };
+ b.ReplicaPorts.Add(b.Port.Value);
+ proxyDescription.Bindings.Add(b);
+ }
+ var proxyContanierService = new Service(proxyDescription);
+ containers.Add(proxyContanierService);
+ proxies.Add(proxyContanierService);
+ }
+
string? dockerNetwork = null;
if (!string.IsNullOrEmpty(application.Network))
@@ -91,6 +134,9 @@ namespace Microsoft.Tye.Hosting
await ProcessUtil.RunAsync("docker", command);
}
+ // Stash information outside of the application services
+ application.Items[typeof(DockerApplicationInformation)] = new DockerApplicationInformation(dockerNetwork, proxies);
+
var tasks = new Task[containers.Count];
var index = 0;
@@ -106,23 +152,34 @@ namespace Microsoft.Tye.Hosting
public async Task StopAsync(Application application)
{
+ if (!application.Items.TryGetValue(typeof(DockerApplicationInformation), out var value))
+ {
+ return;
+ }
+
+ var info = (DockerApplicationInformation)value;
+
var services = application.Services;
var index = 0;
- var tasks = new Task[services.Count];
+ var tasks = new Task[services.Count + info.Proxies.Count];
foreach (var s in services.Values)
{
- var state = s;
- tasks[index++] = StopContainerAsync(state);
+ tasks[index++] = StopContainerAsync(s);
+ }
+
+ foreach (var s in info.Proxies)
+ {
+ tasks[index++] = StopContainerAsync(s);
}
await Task.WhenAll(tasks);
- if (string.IsNullOrEmpty(application.Network) && application.Items.TryGetValue("dockerNetwork", out var dockerNetwork))
+ if (string.IsNullOrEmpty(application.Network) && !string.IsNullOrEmpty(info.DockerNetwork))
{
- _logger.LogInformation("Removing docker network {Network}", dockerNetwork);
+ _logger.LogInformation("Removing docker network {Network}", info.DockerNetwork);
- var command = $"network rm {dockerNetwork}";
+ var command = $"network rm {info.DockerNetwork}";
_logger.LogInformation("Running docker command {Command}", command);
@@ -189,7 +246,7 @@ namespace Microsoft.Tye.Hosting
// These are the ports that the application should use for binding
// 1. Tell the docker container what port to bind to
- portString = string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.ContainerPort ?? p.Port}"));
+ portString = docker.Private ? "" : string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.ContainerPort ?? p.Port}"));
// 2. Configure ASP.NET Core to bind to those same ports
environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.ContainerPort ?? p.Port}"));
@@ -206,6 +263,9 @@ namespace Microsoft.Tye.Hosting
// 3. For non-ASP.NET Core apps, pass the same information in the PORT env variable as a semicolon separated list.
environment["PORT"] = string.Join(";", ports.Select(p => $"{p.ContainerPort ?? p.Port}"));
+
+ // This the port for the container proxy (containerport:externalport)
+ environment["PROXY_PORT"] = string.Join(";", ports.Select(p => $"{p.ContainerPort ?? p.Port}:{p.ExternalPort}"));
}
// See: https://github.com/docker/for-linux/issues/264
@@ -215,6 +275,7 @@ namespace Microsoft.Tye.Hosting
application.PopulateEnvironment(service, (key, value) => environment[key] = value, hostname);
environment["APP_INSTANCE"] = replica;
+ environment["CONTAINER_HOST"] = hostname;
status.Environment = environment;
@@ -284,9 +345,9 @@ namespace Microsoft.Tye.Hosting
if (!string.IsNullOrEmpty(dockerNetwork))
{
- status.DockerNetworkAlias = serviceDescription.Name;
+ status.DockerNetworkAlias = docker.NetworkAlias ?? serviceDescription.Name;
- var networkCommand = $"network connect {dockerNetwork} {replica} --alias {serviceDescription.Name}";
+ var networkCommand = $"network connect {dockerNetwork} {replica} --alias {status.DockerNetworkAlias}";
service.Logs.OnNext($"[{replica}]: docker {networkCommand}");
@@ -478,5 +539,18 @@ namespace Microsoft.Tye.Hosting
public Task[] Tasks { get; }
public CancellationTokenSource StoppingTokenSource { get; } = new CancellationTokenSource();
}
+
+ private class DockerApplicationInformation
+ {
+ public DockerApplicationInformation(string? dockerNetwork, List proxies)
+ {
+ DockerNetwork = dockerNetwork;
+ Proxies = proxies;
+ }
+
+ public string? DockerNetwork { get; set; }
+
+ public List Proxies { get; }
+ }
}
}
diff --git a/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj b/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj
index 10d54720..c7ce45bf 100644
--- a/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj
+++ b/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj
@@ -31,6 +31,7 @@
+
diff --git a/src/Microsoft.Tye.Hosting/Model/Application.cs b/src/Microsoft.Tye.Hosting/Model/Application.cs
index 7469233d..654df917 100644
--- a/src/Microsoft.Tye.Hosting/Model/Application.cs
+++ b/src/Microsoft.Tye.Hosting/Model/Application.cs
@@ -112,16 +112,14 @@ namespace Microsoft.Tye.Hosting.Model
if (b.Port != null)
{
- var port = (service.Description.RunInfo is DockerRunInfo &&
- targetService.RunInfo is DockerRunInfo) ? b.ContainerPort ?? b.Port.Value : b.Port.Value;
+ var port = (service.Description.RunInfo is DockerRunInfo) ? b.ContainerPort ?? b.Port.Value : b.Port.Value;
set($"SERVICE__{configName}__PORT", port.ToString());
set($"{envName}_SERVICE_PORT", port.ToString());
}
// Use the container name as the host name if there's a single replica (current limitation)
- var host = b.Host ?? (service.Description.RunInfo is DockerRunInfo &&
- targetService.RunInfo is DockerRunInfo ? targetService.Name : defaultHost);
+ var host = b.Host ?? (service.Description.RunInfo is DockerRunInfo ? targetService.Name : defaultHost);
set($"SERVICE__{configName}__HOST", host);
set($"{envName}_SERVICE_HOST", host);
diff --git a/src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs b/src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs
index 91f8cec5..6d56ecae 100644
--- a/src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs
+++ b/src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs
@@ -14,6 +14,10 @@ namespace Microsoft.Tye.Hosting.Model
Args = args;
}
+ public bool Private { get; set; }
+
+ public string? NetworkAlias { get; set; }
+
public string? WorkingDirectory { get; set; }
public List VolumeMappings { get; } = new List();
diff --git a/src/Microsoft.Tye.Hosting/PortAssigner.cs b/src/Microsoft.Tye.Hosting/PortAssigner.cs
index e2778041..3ef5b0af 100644
--- a/src/Microsoft.Tye.Hosting/PortAssigner.cs
+++ b/src/Microsoft.Tye.Hosting/PortAssigner.cs
@@ -78,22 +78,18 @@ namespace Microsoft.Tye.Hosting
binding.Name ?? binding.Protocol);
}
- // Only set the container port if we're running in a container
- if (service.Description.RunInfo is DockerRunInfo)
- {
- var httpBinding = service.Description.Bindings.FirstOrDefault(b => b.Protocol == "http");
- var httpsBinding = service.Description.Bindings.FirstOrDefault(b => b.Protocol == "https");
+ var httpBinding = service.Description.Bindings.FirstOrDefault(b => b.Protocol == "http");
+ var httpsBinding = service.Description.Bindings.FirstOrDefault(b => b.Protocol == "https");
- // Default the first http and https port to 80 and 443
- if (httpBinding != null)
- {
- httpBinding.ContainerPort ??= 80;
- }
+ // Default the first http and https port to 80 and 443
+ if (httpBinding != null)
+ {
+ httpBinding.ContainerPort ??= 80;
+ }
- if (httpsBinding != null)
- {
- httpsBinding.ContainerPort ??= 443;
- }
+ if (httpsBinding != null)
+ {
+ httpsBinding.ContainerPort ??= 443;
}
}
diff --git a/src/Microsoft.Tye.Hosting/ProxyService.cs b/src/Microsoft.Tye.Hosting/ProxyService.cs
index a49288a2..2723c1b2 100644
--- a/src/Microsoft.Tye.Hosting/ProxyService.cs
+++ b/src/Microsoft.Tye.Hosting/ProxyService.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Bedrock.Framework;
@@ -59,7 +60,12 @@ namespace Microsoft.Tye.Hosting
var ports = binding.ReplicaPorts;
- sockets.Listen(IPAddress.Loopback, binding.Port.Value, o =>
+ // We need to bind to all interfaces on linux since the container -> host communication won't work
+ // if we use the IP address to reach out of the host. This works fine on osx and windows
+ // but doesn't work on linux.
+ var host = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? IPAddress.Any : IPAddress.Loopback;
+
+ sockets.Listen(host, binding.Port.Value, o =>
{
long count = 0;
diff --git a/src/Microsoft.Tye.Hosting/TyeHost.cs b/src/Microsoft.Tye.Hosting/TyeHost.cs
index 73fb033c..a56e8c3c 100644
--- a/src/Microsoft.Tye.Hosting/TyeHost.cs
+++ b/src/Microsoft.Tye.Hosting/TyeHost.cs
@@ -281,7 +281,6 @@ namespace Microsoft.Tye.Hosting
{
await _processor.StopAsync(_application);
}
- _processor = null;
}
catch (Exception ex)
{
@@ -295,6 +294,8 @@ namespace Microsoft.Tye.Hosting
await DashboardWebApplication.StopAsync();
}
}
+
+ _processor = null;
}
public async ValueTask DisposeAsync()
diff --git a/src/Microsoft.Tye.Proxy/Microsoft.Tye.Proxy.csproj b/src/Microsoft.Tye.Proxy/Microsoft.Tye.Proxy.csproj
new file mode 100644
index 00000000..2e1f5e27
--- /dev/null
+++ b/src/Microsoft.Tye.Proxy/Microsoft.Tye.Proxy.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Tye.Proxy/Program.cs b/src/Microsoft.Tye.Proxy/Program.cs
new file mode 100644
index 00000000..fb5e2ce1
--- /dev/null
+++ b/src/Microsoft.Tye.Proxy/Program.cs
@@ -0,0 +1,135 @@
+using System;
+using System.IO;
+using System.IO.Pipelines;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Bedrock.Framework;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Tye.Proxy
+{
+ public class Program
+ {
+ static async Task Main(string[] args)
+ {
+ var serviceName = Environment.GetEnvironmentVariable("APP_INSTANCE");
+ var containerHost = Environment.GetEnvironmentVariable("CONTAINER_HOST");
+
+ using var host = new HostBuilder()
+ .ConfigureLogging(logging =>
+ {
+ logging.AddConsole();
+ logging.SetMinimumLevel(LogLevel.Debug);
+ })
+ .ConfigureServer(server =>
+ {
+ var logger = server.ApplicationServices.GetRequiredService>();
+
+ logger.LogInformation("Received connection information {Host}:{Port}", containerHost, Environment.GetEnvironmentVariable("PROXY_PORT"));
+
+ static (int Port, int ExternalPort) ResolvePort(string portValue)
+ {
+ var pair = portValue.Split(':');
+ return (int.Parse(pair[0]), int.Parse(pair[1]));
+ }
+
+ var ports = Environment.GetEnvironmentVariable("PROXY_PORT")?.Split(';').Select(ResolvePort) ?? Enumerable.Empty<(int, int)>();
+
+ server.UseSockets(sockets =>
+ {
+ foreach (var mapping in ports)
+ {
+ sockets.Listen(IPAddress.Any, mapping.Port, o =>
+ {
+ // o.UseConnectionLogging("Microsoft.Tye.Proxy");
+
+ o.Run(async connection =>
+ {
+ var notificationFeature = connection.Features.Get();
+
+ NetworkStream? targetStream = null;
+
+ try
+ {
+ var target = new Socket(SocketType.Stream, ProtocolType.Tcp)
+ {
+ NoDelay = true
+ };
+
+ logger.LogDebug("Attempting to connect to {ServiceName} listening on {Port}:{ExternalPort}", serviceName, mapping.Port, mapping.ExternalPort);
+
+ await target.ConnectAsync(containerHost, mapping.ExternalPort);
+
+ logger.LogDebug("Successfully connected to {ServiceName} listening on {Port}:{ExternalPort}", serviceName, mapping.Port, mapping.ExternalPort);
+
+ targetStream = new NetworkStream(target, ownsSocket: true);
+ }
+ catch (Exception ex)
+ {
+ logger.LogDebug(ex, "Proxy error for service {ServiceName}", serviceName);
+
+ if (targetStream is object)
+ {
+ await targetStream.DisposeAsync();
+ }
+
+ connection.Abort();
+ return;
+ }
+
+ try
+ {
+ logger.LogDebug("Proxying traffic to {ServiceName} {Port}:{ExternalPort}", serviceName, mapping.Port, mapping.ExternalPort);
+
+ // external -> internal
+ var reading = Task.Run(() => connection.Transport.Input.CopyToAsync(targetStream, notificationFeature.ConnectionClosedRequested));
+
+ // internal -> external
+ var writing = Task.Run(() => targetStream.CopyToAsync(connection.Transport.Output, notificationFeature.ConnectionClosedRequested));
+
+ await Task.WhenAll(reading, writing);
+ }
+ catch (ConnectionResetException)
+ {
+ // Connection was reset
+ }
+ catch (IOException)
+ {
+ // Reset can also appear as an IOException with an inner SocketException
+ }
+ catch (OperationCanceledException ex)
+ {
+ if (!notificationFeature.ConnectionClosedRequested.IsCancellationRequested)
+ {
+ logger.LogDebug(0, ex, "Proxy error for service {ServiceName}", serviceName);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogDebug(0, ex, "Proxy error for service {ServiceName}", serviceName);
+ }
+ finally
+ {
+ await targetStream.DisposeAsync();
+ }
+
+ // This needs to reconnect to the target port(s) until its bound
+ // it has to stop if the service is no longer running
+ });
+
+ });
+ }
+ });
+ })
+ .Build();
+
+ await host.RunAsync();
+ }
+ }
+}
diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs
index 9848b137..2ca7803b 100644
--- a/test/E2ETest/TyeRunTests.cs
+++ b/test/E2ETest/TyeRunTests.cs
@@ -439,9 +439,15 @@ namespace E2ETest
await RunHostingApplication(application, Array.Empty(), async (app, uri) =>
{
- using var client = new HttpClient();
-
var ingressUri = await GetServiceUrl(client, uri, "ingress");
+ var appAUri = await GetServiceUrl(client, uri, "appA");
+ var appBUri = await GetServiceUrl(client, uri, "appB");
+
+ var appAResponse = await client.GetAsync(appAUri);
+ var appBResponse = await client.GetAsync(appBUri);
+
+ Assert.True(appAResponse.IsSuccessStatusCode);
+ Assert.True(appBResponse.IsSuccessStatusCode);
var responseA = await client.GetAsync(ingressUri + "/A");
var responseB = await client.GetAsync(ingressUri + "/B");
@@ -462,6 +468,46 @@ namespace E2ETest
});
}
+ [ConditionalFact]
+ [SkipIfDockerNotRunning]
+ public async Task NginxIngressTest()
+ {
+ using var projectDirectory = CopySampleProjectDirectory("nginx-ingress");
+
+ var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
+ var outputContext = new OutputContext(_sink, Verbosity.Debug);
+ var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
+
+ var handler = new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
+ AllowAutoRedirect = false
+ };
+
+ var client = new HttpClient(new RetryHandler(handler));
+
+ await RunHostingApplication(application, Array.Empty(), async (app, uri) =>
+ {
+ var nginxUri = await GetServiceUrl(client, uri, "nginx");
+ var appAUri = await GetServiceUrl(client, uri, "appA");
+ var appBUri = await GetServiceUrl(client, uri, "appB");
+
+ var nginxResponse = await client.GetAsync(nginxUri);
+ var appAResponse = await client.GetAsync(appAUri);
+ var appBResponse = await client.GetAsync(appBUri);
+
+ Assert.Equal(HttpStatusCode.NotFound, nginxResponse.StatusCode);
+ Assert.True(appAResponse.IsSuccessStatusCode);
+ Assert.True(appBResponse.IsSuccessStatusCode);
+
+ var responseA = await client.GetAsync(nginxUri + "/A");
+ var responseB = await client.GetAsync(nginxUri + "/B");
+
+ Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync());
+ Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync());
+ });
+ }
+
[Fact]
public async Task NullDebugTargetsDoesNotThrow()
{
diff --git a/tye.sln b/tye.sln
index 766b7725..572423bc 100644
--- a/tye.sln
+++ b/tye.sln
@@ -11,17 +11,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F19B02EB-A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "E2ETest", "test\E2ETest\E2ETest.csproj", "{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Hosting", "src\Microsoft.Tye.Hosting\Microsoft.Tye.Hosting.csproj", "{0F4F5A86-DD27-4AF9-BF97-221EAD5040E7}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Hosting", "src\Microsoft.Tye.Hosting\Microsoft.Tye.Hosting.csproj", "{0F4F5A86-DD27-4AF9-BF97-221EAD5040E7}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Hosting.Diagnostics", "src\Microsoft.Tye.Hosting.Diagnostics\Microsoft.Tye.Hosting.Diagnostics.csproj", "{CEBFC149-8162-4A0A-9AD4-40498B9172CD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Hosting.Diagnostics", "src\Microsoft.Tye.Hosting.Diagnostics\Microsoft.Tye.Hosting.Diagnostics.csproj", "{CEBFC149-8162-4A0A-9AD4-40498B9172CD}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Hosting.Runtime", "src\Microsoft.Tye.Hosting.Runtime\Microsoft.Tye.Hosting.Runtime.csproj", "{34719884-1338-4965-BA2A-F98DB03733C2}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Hosting.Runtime", "src\Microsoft.Tye.Hosting.Runtime\Microsoft.Tye.Hosting.Runtime.csproj", "{34719884-1338-4965-BA2A-F98DB03733C2}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tye.Core", "src\Microsoft.Tye.Core\Microsoft.Tye.Core.csproj", "{D0359C69-6EA9-4B03-9455-90E8E04F1CB0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Core", "src\Microsoft.Tye.Core\Microsoft.Tye.Core.csproj", "{D0359C69-6EA9-4B03-9455-90E8E04F1CB0}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Tye.Extensions", "src\Microsoft.Tye.Extensions\Microsoft.Tye.Extensions.csproj", "{AAF0CE0B-E53A-4E10-AA82-BF7200AB2B0C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Extensions", "src\Microsoft.Tye.Extensions\Microsoft.Tye.Extensions.csproj", "{AAF0CE0B-E53A-4E10-AA82-BF7200AB2B0C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Tye.Extensions.Configuration", "src\Microsoft.Tye.Extensions.Configuration\Microsoft.Tye.Extensions.Configuration.csproj", "{B07394E4-30A7-429A-BC5A-747B54D5A447}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Extensions.Configuration", "src\Microsoft.Tye.Extensions.Configuration\Microsoft.Tye.Extensions.Configuration.csproj", "{B07394E4-30A7-429A-BC5A-747B54D5A447}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Tye.Proxy", "src\Microsoft.Tye.Proxy\Microsoft.Tye.Proxy.csproj", "{7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -129,6 +131,18 @@ Global
{B07394E4-30A7-429A-BC5A-747B54D5A447}.Release|x64.Build.0 = Release|Any CPU
{B07394E4-30A7-429A-BC5A-747B54D5A447}.Release|x86.ActiveCfg = Release|Any CPU
{B07394E4-30A7-429A-BC5A-747B54D5A447}.Release|x86.Build.0 = Release|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Debug|x64.Build.0 = Debug|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Debug|x86.Build.0 = Debug|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|x64.ActiveCfg = Release|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|x64.Build.0 = Release|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|x86.ActiveCfg = Release|Any CPU
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -142,6 +156,7 @@ Global
{D0359C69-6EA9-4B03-9455-90E8E04F1CB0} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
{AAF0CE0B-E53A-4E10-AA82-BF7200AB2B0C} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
{B07394E4-30A7-429A-BC5A-747B54D5A447} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
+ {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D8002603-BB27-4500-BF86-274A8E72D302}