Tye is a tool that makes developing, testing, and deploying microservices and distributed applications easier. Project Tye includes a local orchestrator to make developing microservices easier and the ability to deploy microservices to Kubernetes with min
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

184 lines
6.9 KiB

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Proxy;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Tye.Hosting.Model;
namespace Microsoft.Tye.Hosting
{
public partial class HttpProxyService : IApplicationProcessor
{
private List<WebApplication> _webApplications = new List<WebApplication>();
private readonly ILogger _logger;
public HttpProxyService(ILogger logger)
{
_logger = logger;
}
public async Task StartAsync(Application application)
{
var invoker = new HttpMessageInvoker(new ConnectionRetryHandler(new SocketsHttpHandler
{
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
UseProxy = false
}));
foreach (var service in application.Services.Values)
{
var serviceDescription = service.Description;
if (service.Description.RunInfo is IngressRunInfo runInfo)
{
var builder = new WebApplicationBuilder();
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));
}
_logger.LogInformation("Ingress bound to {Addresses}", string.Join(", ", addresses));
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;
}
var targetServiceDescription = target.Description;
var uris = new List<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
foreach (var port in targetBinding.ReplicaPorts)
{
var url = $"{targetBinding.Protocol}://localhost:{port}";
uris.Add(new Uri(url));
}
// The only load balancing strategy here is round robin
long count = 0;
RequestDelegate del = context =>
{
var next = (int)(Interlocked.Increment(ref count) % uris.Count);
var uri = new UriBuilder(uris[next])
{
Path = rule.PreservePath ? context.Request.Path.ToString() : (string)context.Request.RouteValues["path"] ?? "/"
};
return context.ProxyRequest(invoker, uri.Uri);
};
IEndpointConventionBuilder conventions = null!;
if (rule.Path != null)
{
conventions = ((IEndpointRouteBuilder)webApp).Map(rule.Path.TrimEnd('/') + "/{**path}", del);
}
else
{
conventions = webApp.MapFallback(del);
}
if (rule.Host != null)
{
conventions.WithMetadata(new IngressHostMetadata(rule.Host));
}
conventions.WithDisplayName(rule.Service);
}
await webApp.StartAsync();
foreach (var replica in service.Replicas)
{
service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, replica.Value));
}
}
}
}
public async Task StopAsync(Application application)
{
foreach (var webApp in _webApplications)
{
try
{
await webApp.StopAsync();
}
catch (OperationCanceledException)
{
}
finally
{
webApp.Dispose();
}
}
}
}
}