|
|
@ -12,11 +12,13 @@ using System.Threading; |
|
|
using System.Threading.Tasks; |
|
|
using System.Threading.Tasks; |
|
|
using Microsoft.AspNetCore.Builder; |
|
|
using Microsoft.AspNetCore.Builder; |
|
|
using Microsoft.AspNetCore.Hosting; |
|
|
using Microsoft.AspNetCore.Hosting; |
|
|
|
|
|
using Microsoft.AspNetCore.Hosting.Server.Features; |
|
|
using Microsoft.AspNetCore.Http; |
|
|
using Microsoft.AspNetCore.Http; |
|
|
using Microsoft.AspNetCore.Proxy; |
|
|
using Microsoft.AspNetCore.Proxy; |
|
|
using Microsoft.AspNetCore.Routing; |
|
|
using Microsoft.AspNetCore.Routing; |
|
|
using Microsoft.AspNetCore.Routing.Matching; |
|
|
using Microsoft.AspNetCore.Routing.Matching; |
|
|
using Microsoft.Extensions.DependencyInjection; |
|
|
using Microsoft.Extensions.DependencyInjection; |
|
|
|
|
|
using Microsoft.Extensions.Hosting; |
|
|
using Microsoft.Extensions.Logging; |
|
|
using Microsoft.Extensions.Logging; |
|
|
using Microsoft.Tye.Hosting.Model; |
|
|
using Microsoft.Tye.Hosting.Model; |
|
|
|
|
|
|
|
|
@ -24,7 +26,7 @@ namespace Microsoft.Tye.Hosting |
|
|
{ |
|
|
{ |
|
|
public partial class HttpProxyService : IApplicationProcessor |
|
|
public partial class HttpProxyService : IApplicationProcessor |
|
|
{ |
|
|
{ |
|
|
private List<WebApplication> _webApplications = new List<WebApplication>(); |
|
|
private List<IHost> _webApplications = new List<IHost>(); |
|
|
private readonly ILogger _logger; |
|
|
private readonly ILogger _logger; |
|
|
|
|
|
|
|
|
private ConcurrentDictionary<int, bool> _readyPorts; |
|
|
private ConcurrentDictionary<int, bool> _readyPorts; |
|
|
@ -50,126 +52,158 @@ namespace Microsoft.Tye.Hosting |
|
|
|
|
|
|
|
|
if (service.Description.RunInfo is IngressRunInfo runInfo) |
|
|
if (service.Description.RunInfo is IngressRunInfo runInfo) |
|
|
{ |
|
|
{ |
|
|
var builder = new WebApplicationBuilder(); |
|
|
var host = Host.CreateDefaultBuilder() |
|
|
|
|
|
.ConfigureWebHostDefaults(builder => |
|
|
builder.Services.AddSingleton<MatcherPolicy, IngressHostMatcherPolicy>(); |
|
|
|
|
|
|
|
|
|
|
|
builder.Logging.AddProvider(new ServiceLoggerProvider(service.Logs)); |
|
|
|
|
|
|
|
|
|
|
|
var addresses = new List<string>(); |
|
|
|
|
|
|
|
|
|
|
|
// Bind to the addresses on this resource
|
|
|
|
|
|
for (int i = 0; i < serviceDescription.Replicas; i++) |
|
|
|
|
|
{ |
|
|
|
|
|
// Fake replicas since it's all running processes
|
|
|
|
|
|
var replica = service.Description.Name + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); |
|
|
|
|
|
var status = new IngressStatus(service, replica); |
|
|
|
|
|
service.Replicas[replica] = status; |
|
|
|
|
|
|
|
|
|
|
|
var ports = new List<int>(); |
|
|
|
|
|
|
|
|
|
|
|
foreach (var binding in serviceDescription.Bindings) |
|
|
|
|
|
{ |
|
|
|
|
|
if (binding.Port == null) |
|
|
|
|
|
{ |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var port = binding.ReplicaPorts[i]; |
|
|
|
|
|
ports.Add(port); |
|
|
|
|
|
var url = $"{binding.Protocol}://localhost:{port}"; |
|
|
|
|
|
addresses.Add(url); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
status.Ports = ports; |
|
|
|
|
|
|
|
|
|
|
|
service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
builder.Server.UseUrls(addresses.ToArray()); |
|
|
|
|
|
var webApp = builder.Build(); |
|
|
|
|
|
|
|
|
|
|
|
_webApplications.Add(webApp); |
|
|
|
|
|
|
|
|
|
|
|
// For each ingress rule, bind to the path and host
|
|
|
|
|
|
foreach (var rule in runInfo.Rules) |
|
|
|
|
|
{ |
|
|
|
|
|
if (!application.Services.TryGetValue(rule.Service, out var target)) |
|
|
|
|
|
{ |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Processing ingress rule: Path:{Path}, Host:{Host}, Service:{Service}", rule.Path, rule.Host, rule.Service); |
|
|
|
|
|
|
|
|
|
|
|
var targetServiceDescription = target.Description; |
|
|
|
|
|
RegisterListener(target); |
|
|
|
|
|
|
|
|
|
|
|
var uris = new List<(int Port, Uri Uri)>(); |
|
|
|
|
|
|
|
|
|
|
|
// HTTP before HTTPS (this might change once we figure out certs...)
|
|
|
|
|
|
var targetBinding = targetServiceDescription.Bindings.FirstOrDefault(b => b.Protocol == "http") ?? |
|
|
|
|
|
targetServiceDescription.Bindings.FirstOrDefault(b => b.Protocol == "https"); |
|
|
|
|
|
|
|
|
|
|
|
if (targetBinding == null) |
|
|
|
|
|
{ |
|
|
{ |
|
|
_logger.LogInformation("Service {ServiceName} does not have any HTTP or HTTPs bindings", targetServiceDescription.Name); |
|
|
var urls = new List<string>(); |
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// For each of the target service replicas, get the base URL
|
|
|
// Bind to the addresses on this resource
|
|
|
// based on the replica port
|
|
|
for (int i = 0; i < serviceDescription.Replicas; i++) |
|
|
for (int i = 0; i < targetServiceDescription.Replicas; i++) |
|
|
{ |
|
|
{ |
|
|
// Fake replicas since it's all running processes
|
|
|
var port = targetBinding.ReplicaPorts[i]; |
|
|
var replica = service.Description.Name + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); |
|
|
var url = $"{targetBinding.Protocol}://localhost:{port}"; |
|
|
var status = new IngressStatus(service, replica); |
|
|
uris.Add((port, new Uri(url))); |
|
|
service.Replicas[replica] = status; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Service {ServiceName} is using {Urls}", targetServiceDescription.Name, string.Join(",", uris.Select(u => u.ToString()))); |
|
|
|
|
|
|
|
|
|
|
|
// The only load balancing strategy here is round robin
|
|
|
var ports = new List<int>(); |
|
|
long count = 0; |
|
|
|
|
|
RequestDelegate del = async context => |
|
|
|
|
|
{ |
|
|
|
|
|
var next = (int)(Interlocked.Increment(ref count) % uris.Count); |
|
|
|
|
|
|
|
|
|
|
|
// we find the first `Ready` port
|
|
|
foreach (var binding in serviceDescription.Bindings) |
|
|
for (int i = 0; i < uris.Count; i++) |
|
|
|
|
|
{ |
|
|
|
|
|
if (_readyPorts.ContainsKey(uris[next].Port)) |
|
|
|
|
|
{ |
|
|
{ |
|
|
break; |
|
|
if (binding.Port == null) |
|
|
|
|
|
{ |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var port = binding.ReplicaPorts[i]; |
|
|
|
|
|
ports.Add(port); |
|
|
|
|
|
var url = $"{binding.Protocol}://localhost:{port}"; |
|
|
|
|
|
urls.Add(url); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
next = (int)(Interlocked.Increment(ref count) % uris.Count); |
|
|
status.Ports = ports; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// if we've looped through all the port and didn't find a single one that is `Ready`, we return HTTP BadGateway
|
|
|
service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); |
|
|
if (!_readyPorts.ContainsKey(uris[next].Port)) |
|
|
|
|
|
{ |
|
|
|
|
|
context.Response.StatusCode = (int)HttpStatusCode.BadGateway; |
|
|
|
|
|
await context.Response.WriteAsync("Bad gateway"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
} |
|
|
var uri = new UriBuilder(uris[next].Uri) |
|
|
|
|
|
|
|
|
builder.ConfigureServices(services => |
|
|
{ |
|
|
{ |
|
|
Path = rule.PreservePath ? $"{context.Request.Path}" : (string)context.Request.RouteValues["path"] ?? "/", |
|
|
services.AddSingleton<MatcherPolicy, IngressHostMatcherPolicy>(); |
|
|
Query = context.Request.QueryString.Value |
|
|
services.AddLogging(loggingBuilder => |
|
|
}; |
|
|
{ |
|
|
|
|
|
loggingBuilder.AddProvider(new ServiceLoggerProvider(service.Logs)); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
await context.ProxyRequest(invoker, uri.Uri); |
|
|
services.Configure<IServerAddressesFeature>(serverAddresses => |
|
|
}; |
|
|
{ |
|
|
|
|
|
var addresses = serverAddresses.Addresses; |
|
|
|
|
|
if (addresses.IsReadOnly) |
|
|
|
|
|
{ |
|
|
|
|
|
throw new NotSupportedException("Changing the URL isn't supported."); |
|
|
|
|
|
} |
|
|
|
|
|
addresses.Clear(); |
|
|
|
|
|
foreach (var u in urls) |
|
|
|
|
|
{ |
|
|
|
|
|
addresses.Add(u); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
builder.UseUrls(urls.ToArray()); |
|
|
|
|
|
|
|
|
|
|
|
builder.Configure(app => |
|
|
|
|
|
{ |
|
|
|
|
|
app.UseRouting(); |
|
|
|
|
|
|
|
|
IEndpointConventionBuilder conventions = |
|
|
app.UseEndpoints(endpointBuilder => |
|
|
((IEndpointRouteBuilder)webApp).Map((rule.Path?.TrimEnd('/') ?? "") + "/{**path}", del); |
|
|
{ |
|
|
|
|
|
foreach (var rule in runInfo.Rules) |
|
|
|
|
|
{ |
|
|
|
|
|
if (!application.Services.TryGetValue(rule.Service, out var target)) |
|
|
|
|
|
{ |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Processing ingress rule: Path:{Path}, Host:{Host}, Service:{Service}", rule.Path, rule.Host, rule.Service); |
|
|
|
|
|
|
|
|
|
|
|
var targetServiceDescription = target.Description; |
|
|
|
|
|
RegisterListener(target); |
|
|
|
|
|
|
|
|
|
|
|
var uris = new List<(int Port, Uri Uri)>(); |
|
|
|
|
|
|
|
|
|
|
|
// HTTP before HTTPS (this might change once we figure out certs...)
|
|
|
|
|
|
var targetBinding = targetServiceDescription.Bindings.FirstOrDefault(b => b.Protocol == "http") ?? |
|
|
|
|
|
targetServiceDescription.Bindings.FirstOrDefault(b => b.Protocol == "https"); |
|
|
|
|
|
|
|
|
|
|
|
if (targetBinding == null) |
|
|
|
|
|
{ |
|
|
|
|
|
_logger.LogInformation("Service {ServiceName} does not have any HTTP or HTTPs bindings", targetServiceDescription.Name); |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// For each of the target service replicas, get the base URL
|
|
|
|
|
|
// based on the replica port
|
|
|
|
|
|
for (int i = 0; i < targetServiceDescription.Replicas; i++) |
|
|
|
|
|
{ |
|
|
|
|
|
var port = targetBinding.ReplicaPorts[i]; |
|
|
|
|
|
var url = $"{targetBinding.Protocol}://localhost:{port}"; |
|
|
|
|
|
uris.Add((port, new Uri(url))); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Service {ServiceName} is using {Urls}", targetServiceDescription.Name, string.Join(",", uris.Select(u => u.ToString()))); |
|
|
|
|
|
|
|
|
|
|
|
// The only load balancing strategy here is round robin
|
|
|
|
|
|
long count = 0; |
|
|
|
|
|
RequestDelegate del = async context => |
|
|
|
|
|
{ |
|
|
|
|
|
var next = (int)(Interlocked.Increment(ref count) % uris.Count); |
|
|
|
|
|
|
|
|
|
|
|
// we find the first `Ready` port
|
|
|
|
|
|
for (int i = 0; i < uris.Count; i++) |
|
|
|
|
|
{ |
|
|
|
|
|
if (_readyPorts.ContainsKey(uris[next].Port)) |
|
|
|
|
|
{ |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
next = (int)(Interlocked.Increment(ref count) % uris.Count); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// if we've looped through all the port and didn't find a single one that is `Ready`, we return HTTP BadGateway
|
|
|
|
|
|
if (!_readyPorts.ContainsKey(uris[next].Port)) |
|
|
|
|
|
{ |
|
|
|
|
|
context.Response.StatusCode = (int)HttpStatusCode.BadGateway; |
|
|
|
|
|
await context.Response.WriteAsync("Bad gateway"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
var uri = new UriBuilder(uris[next].Uri) |
|
|
|
|
|
{ |
|
|
|
|
|
Path = rule.PreservePath ? $"{context.Request.Path}" : (string)context.Request.RouteValues["path"] ?? "/", |
|
|
|
|
|
Query = context.Request.QueryString.Value |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
await context.ProxyRequest(invoker, uri.Uri); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
IEndpointConventionBuilder conventions = |
|
|
|
|
|
endpointBuilder.Map((rule.Path?.TrimEnd('/') ?? "") + "/{**path}", del); |
|
|
|
|
|
|
|
|
|
|
|
if (rule.Host != null) |
|
|
|
|
|
{ |
|
|
|
|
|
conventions.WithMetadata(new IngressHostMetadata(rule.Host)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
conventions.WithDisplayName(rule.Service); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var webApp = host.Build(); |
|
|
|
|
|
|
|
|
if (rule.Host != null) |
|
|
_webApplications.Add(webApp); |
|
|
{ |
|
|
|
|
|
conventions.WithMetadata(new IngressHostMetadata(rule.Host)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
conventions.WithDisplayName(rule.Service); |
|
|
// For each ingress rule, bind to the path and host
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await webApp.StartAsync(); |
|
|
await webApp.StartAsync(); |
|
|
|
|
|
|
|
|
|