diff --git a/src/Microsoft.Tye.Hosting/PortAssigner.cs b/src/Microsoft.Tye.Hosting/PortAssigner.cs new file mode 100644 index 00000000..76ca1200 --- /dev/null +++ b/src/Microsoft.Tye.Hosting/PortAssigner.cs @@ -0,0 +1,108 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.Tye.Hosting.Model; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Tye.Hosting +{ + public class PortAssigner : IApplicationProcessor + { + private readonly ILogger _logger; + + public PortAssigner(ILogger logger) + { + _logger = logger; + } + + public Task StartAsync(Application application) + { + foreach (var service in application.Services.Values) + { + if (service.Description.RunInfo == null) + { + continue; + } + + static int GetNextPort() + { + // Let the OS assign the next available port. Unless we cycle through all ports + // on a test run, the OS will always increment the port number when making these calls. + // This prevents races in parallel test runs where a test is already bound to + // a given port, and a new test is able to bind to the same port due to port + // reuse being enabled by default by the OS. + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + + foreach (var binding in service.Description.Bindings) + { + if (binding.Port == null && !binding.AutoAssignPort) + { + continue; + } + + // Auto assign a port + if (binding.Port == null) + { + binding.Port = GetNextPort(); + } + + if (service.Description.Replicas == 1) + { + // No need to proxy, the port maps to itself + service.PortMap[binding.Port.Value] = new List { binding.Port.Value }; + continue; + } + + var ports = new List(); + + for (var i = 0; i < service.Description.Replicas; i++) + { + // Reserve a port for each replica + var port = GetNextPort(); + ports.Add(port); + } + + _logger.LogInformation( + "Mapping external port {ExternalPort} to internal port(s) {InternalPorts} for {ServiceName} binding {BindingName}", + binding.Port, + string.Join(", ", ports.Select(p => p.ToString())), + service.Description.Name, + binding.Name ?? binding.Protocol); + + service.PortMap[binding.Port.Value] = ports; + } + + 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; + } + + if (httpsBinding != null) + { + httpsBinding.ContainerPort = 443; + } + + } + + return Task.CompletedTask; + } + + public Task StopAsync(Application application) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.Tye.Hosting/ProxyService.cs b/src/Microsoft.Tye.Hosting/ProxyService.cs index a3a610b3..293b5cd5 100644 --- a/src/Microsoft.Tye.Hosting/ProxyService.cs +++ b/src/Microsoft.Tye.Hosting/ProxyService.cs @@ -3,10 +3,8 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.IO; using System.IO.Pipelines; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading; @@ -45,63 +43,21 @@ namespace Microsoft.Tye.Hosting continue; } - static int GetNextPort() - { - // Let the OS assign the next available port. Unless we cycle through all ports - // on a test run, the OS will always increment the port number when making these calls. - // This prevents races in parallel test runs where a test is already bound to - // a given port, and a new test is able to bind to the same port due to port - // reuse being enabled by default by the OS. - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - return ((IPEndPoint)socket.LocalEndPoint).Port; - } - foreach (var binding in service.Description.Bindings) { - if (binding.Port == null && !binding.AutoAssignPort) - { - continue; - } - if (binding.Port == null) { - binding.Port = GetNextPort(); - } - - if (binding.Protocol == "http" || (binding.Protocol == null && service.ServiceType == Model.ServiceType.Project)) - { - binding.ContainerPort = 80; - } - else if (binding.Protocol == "https") - { - binding.ContainerPort = 443; + // There's no port so nothing to proxy + continue; } if (service.Description.Replicas == 1) { - // No need to proxy - service.PortMap[binding.Port.Value] = new List { binding.Port.Value }; + // No need to proxy for a single replica, we may want to do this later but right now we skip it continue; } - var ports = new List(); - - for (var i = 0; i < service.Description.Replicas; i++) - { - // Reserve a port for each replica - var port = GetNextPort(); - ports.Add(port); - } - - _logger.LogInformation( - "Mapping external port {ExternalPort} to internal port(s) {InternalPorts} for {ServiceName} binding {BindingName}", - binding.Port, - string.Join(", ", ports.Select(p => p.ToString())), - service.Description.Name, - binding.Name ?? binding.Protocol); - - service.PortMap[binding.Port.Value] = ports; + var ports = service.PortMap[binding.Port.Value]; sockets.Listen(IPAddress.Loopback, binding.Port.Value, o => { diff --git a/src/Microsoft.Tye.Hosting/TyeHost.cs b/src/Microsoft.Tye.Hosting/TyeHost.cs index c3f4f07d..165ed9fe 100644 --- a/src/Microsoft.Tye.Hosting/TyeHost.cs +++ b/src/Microsoft.Tye.Hosting/TyeHost.cs @@ -257,6 +257,7 @@ namespace Microsoft.Tye.Hosting var processors = new List { new EventPipeDiagnosticsRunner(logger, diagnosticsCollector), + new PortAssigner(logger), new ProxyService(logger), new HttpProxyService(logger), new DockerRunner(logger),