Added container proxy for talking to the host (#292)

* Added container proxy for talking to the host
- This change introduces a container proxy which makes it possible for docker containers can talk to host services using container networking. These proxies will not show up in the dashboard as they are "infrastructure" containers.
- Added Private and NetworkAlias to DockerRunInfo but did not expose these configuration.
- All container communication is done using host names.
- Fix host shutdown again
- Bind to all interfaces on linux
This commit is contained in:
David Fowler 2020-04-02 19:20:28 -07:00 коммит произвёл GitHub
Родитель 9873e05f2f
Коммит d2be7d504f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
27 изменённых файлов: 682 добавлений и 38 удалений

Просмотреть файл

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

Просмотреть файл

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

Просмотреть файл

@ -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"
}
}
}
}

Просмотреть файл

@ -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"));
});
});
}
}
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

Просмотреть файл

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

Просмотреть файл

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

Просмотреть файл

@ -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"
}
}
}
}

Просмотреть файл

@ -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"));
});
});
}
}
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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,11 +29,29 @@ namespace Microsoft.Tye.Hosting
public async Task StopAsync(Application application)
{
var exceptions = new List<Exception>();
// Shutdown in the opposite order
foreach (var processor in _applicationProcessors.Reverse())
{
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);
}
}
}
}

Просмотреть файл

@ -49,6 +49,49 @@ namespace Microsoft.Tye.Hosting
return;
}
var proxies = new List<Service>();
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<Service> proxies)
{
DockerNetwork = dockerNetwork;
Proxies = proxies;
}
public string? DockerNetwork { get; set; }
public List<Service> Proxies { get; }
}
}
}

Просмотреть файл

@ -31,6 +31,7 @@
<ProjectReference Include="..\Microsoft.Tye.Hosting.Diagnostics\Microsoft.Tye.Hosting.Diagnostics.csproj" />
<ProjectReference Include="..\Microsoft.Tye.Core\Microsoft.Tye.Core.csproj" />
<ProjectReference Include="..\Microsoft.Tye.Hosting.Runtime\Microsoft.Tye.Hosting.Runtime.csproj" />
<ProjectReference Include="..\Microsoft.Tye.Proxy\Microsoft.Tye.Proxy.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -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);

Просмотреть файл

@ -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<DockerVolume> VolumeMappings { get; } = new List<DockerVolume>();

Просмотреть файл

@ -78,9 +78,6 @@ 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");
@ -95,7 +92,6 @@ namespace Microsoft.Tye.Hosting
httpsBinding.ContainerPort ??= 443;
}
}
}
return Task.CompletedTask;
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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()

Просмотреть файл

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bedrock.Framework" Version="0.1.38-alpha.gd25d5b37ad" />
<PackageReference Include="System.IO.Pipelines" Version="4.7.0" />
</ItemGroup>
<!-- Include *.deps.json and *.runtimeconfig.json in ContentWithTargetPath so they will be copied to the output folder of projects
that reference this one. -->
<Target Name="AddRuntimeDependenciesToContent" Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp'" BeforeTargets="GetCopyToOutputDirectoryItems">
<ItemGroup>
<ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" />
<ContentWithTargetPath Include="$(ProjectRuntimeConfigFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectRuntimeConfigFileName)" />
</ItemGroup>
</Target>
</Project>

Просмотреть файл

@ -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<ILogger<Program>>();
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<IConnectionLifetimeNotificationFeature>();
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();
}
}
}

Просмотреть файл

@ -439,9 +439,15 @@ namespace E2ETest
await RunHostingApplication(application, Array.Empty<string>(), 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<string>(), 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()
{

27
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}