From 7ea78aaddd5d337036043af33d04f1b31063dda7 Mon Sep 17 00:00:00 2001 From: Jean Llorca Date: Fri, 8 Oct 2021 21:23:57 +0200 Subject: [PATCH] Specify ingress IP in Debug (#1048) * Specify ingress IP in Debug * Quotes yaml values * Stop auto-defaulting specific looback addresses * Update tye YAML schema * Format whitespace --- docs/reference/schema.md | 5 + src/Microsoft.Tye.Core/ApplicationFactory.cs | 1 + .../ConfigModel/ConfigIngressBinding.cs | 1 + src/Microsoft.Tye.Core/CoreStrings.resx | 3 + .../IngressBindingBuilder.cs | 1 + .../Serialization/ConfigIngressParser.cs | 13 +++ src/Microsoft.Tye.Hosting/HttpProxyService.cs | 3 +- .../Model/ServiceBinding.cs | 2 + src/schema/tye-schema.json | 4 + src/tye/ApplicationBuilderExtensions.cs | 1 + test/E2ETest/Microsoft.Tye.E2ETests.csproj | 2 +- test/E2ETest/TyeRunTests.cs | 97 +++++++++++++++++++ .../apps-with-ingress/tye-ip_test.yaml | 20 ++++ 13 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/tye-ip_test.yaml diff --git a/docs/reference/schema.md b/docs/reference/schema.md index ae4bd999..876ffa3f 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -526,6 +526,11 @@ The port of the binding. The protocol (`http` or `https`). +#### `ip` (`string`) + +The optional IP adress to bind to. Can be '*' for all addresses. +Default is localhost. + ## IngressRule `IngressRule` elements appear in an array within the `rules` property of the `Ingress` element. Rules configure the routing behavior of the ingress proxy. diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 37377495..dd126b47 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -461,6 +461,7 @@ namespace Microsoft.Tye Name = configBinding.Name, Port = configBinding.Port, Protocol = configBinding.Protocol ?? "http", + IPAddress = configBinding.IPAddress, }; ingress.Bindings.Add(binding); } diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs index b5fe74bb..a95b88ef 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs @@ -9,5 +9,6 @@ namespace Microsoft.Tye.ConfigModel public string? Name { get; set; } public int? Port { get; set; } public string? Protocol { get; set; } // HTTP or HTTPS + public string? IPAddress { get; set; } // Can be * or any address to listen on } } diff --git a/src/Microsoft.Tye.Core/CoreStrings.resx b/src/Microsoft.Tye.Core/CoreStrings.resx index 350964f0..c1ca0b98 100644 --- a/src/Microsoft.Tye.Core/CoreStrings.resx +++ b/src/Microsoft.Tye.Core/CoreStrings.resx @@ -153,6 +153,9 @@ "{value}" value must be an integer. + + "{value}" value must be an IP address, "*" or "localhost". + "{value}" value cannot be negative. diff --git a/src/Microsoft.Tye.Core/IngressBindingBuilder.cs b/src/Microsoft.Tye.Core/IngressBindingBuilder.cs index 8a3e1cb6..6153b342 100644 --- a/src/Microsoft.Tye.Core/IngressBindingBuilder.cs +++ b/src/Microsoft.Tye.Core/IngressBindingBuilder.cs @@ -9,5 +9,6 @@ namespace Microsoft.Tye public string? Name { get; set; } public int? Port { get; set; } public string? Protocol { get; set; } // HTTP or HTTPS + public string? IPAddress { get; set; } } } diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs index bea20051..602bc02d 100644 --- a/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs @@ -2,7 +2,9 @@ // 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.Net; using Microsoft.Tye.ConfigModel; using YamlDotNet.RepresentationModel; @@ -144,6 +146,17 @@ namespace Tye.Serialization binding.Port = port; break; + case "ip": + if (YamlParser.GetScalarValue(key, child.Value) is string ipString + && (IPAddress.TryParse(ipString, out var ip) || ipString == "*" || ipString.Equals("localhost", StringComparison.OrdinalIgnoreCase))) + { + binding.IPAddress = ipString; + } + else + { + throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeAnIPAddress(key)); + } + break; case "protocol": binding.Protocol = YamlParser.GetScalarValue(key, child.Value); break; diff --git a/src/Microsoft.Tye.Hosting/HttpProxyService.cs b/src/Microsoft.Tye.Hosting/HttpProxyService.cs index cf3b1a08..c132eb6a 100644 --- a/src/Microsoft.Tye.Hosting/HttpProxyService.cs +++ b/src/Microsoft.Tye.Hosting/HttpProxyService.cs @@ -76,7 +76,8 @@ namespace Microsoft.Tye.Hosting var port = binding.ReplicaPorts[i]; ports.Add(port); - var url = $"{binding.Protocol}://localhost:{port}"; + + var url = $"{binding.Protocol}://{binding.IPAddress ?? "localhost"}:{port}"; urls.Add(url); } diff --git a/src/Microsoft.Tye.Hosting/Model/ServiceBinding.cs b/src/Microsoft.Tye.Hosting/Model/ServiceBinding.cs index ac08ea03..2adaa479 100644 --- a/src/Microsoft.Tye.Hosting/Model/ServiceBinding.cs +++ b/src/Microsoft.Tye.Hosting/Model/ServiceBinding.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Net; namespace Microsoft.Tye.Hosting.Model { @@ -13,6 +14,7 @@ namespace Microsoft.Tye.Hosting.Model public int? Port { get; set; } public int? ContainerPort { get; set; } public string? Host { get; set; } + public string? IPAddress { get; set; } public string? Protocol { get; set; } public List ReplicaPorts { get; } = new List(); } diff --git a/src/schema/tye-schema.json b/src/schema/tye-schema.json index 2d8685cf..f7fcf9da 100644 --- a/src/schema/tye-schema.json +++ b/src/schema/tye-schema.json @@ -463,6 +463,10 @@ "protocol": { "description": "The protocol used by the binding", "type": "string" + }, + "ip": { + "description": "The ip address the ingress listens on.", + "type": "string" } } }, diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 34256471..350e860f 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -212,6 +212,7 @@ namespace Microsoft.Tye Name = binding.Name, Port = binding.Port, Protocol = binding.Protocol, + IPAddress = binding.IPAddress, }); } diff --git a/test/E2ETest/Microsoft.Tye.E2ETests.csproj b/test/E2ETest/Microsoft.Tye.E2ETests.csproj index 08c75665..5c5078ce 100644 --- a/test/E2ETest/Microsoft.Tye.E2ETests.csproj +++ b/test/E2ETest/Microsoft.Tye.E2ETests.csproj @@ -45,4 +45,4 @@ - \ No newline at end of file + diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 03261a90..ba2940fa 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -9,6 +9,8 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization; @@ -767,6 +769,101 @@ services: }); } + [Fact] + public async Task IngressSpecificIPTest() + { + var allIps = GetLiveIPAddresses().ToList(); + var testIp = allIps[new Random().Next(allIps.Count)]; + await TestIngressIP($"'{testIp}'", new[] { testIp }, allIps.Where(ip => ip != testIp).Take(1)); + } + + + [Fact] + public async Task IngressAllIPv6Test() + { + var ipV6 = GetLiveIPAddresses(AddressFamily.InterNetworkV6).FirstOrDefault(); + if (ipV6 == null) return; + await TestIngressIP($"'{IPAddress.IPv6Any}'", ipV6); + } + + [Fact] + public async Task IngressAllIPv4Test() + { + var ipV4 = GetLiveIPAddresses(AddressFamily.InterNetwork).FirstOrDefault(); + if (ipV4 == null) return; + var ipV6 = GetLiveIPAddresses(AddressFamily.InterNetworkV6).FirstOrDefault(); + var failIp = ipV6 == null ? Enumerable.Empty() : new[] { ipV6 }; + await TestIngressIP($"'{IPAddress.Any}'", new[] { ipV4 }, failIp); + } + + [Fact] + public async Task IngressAllIPTest() + { + await TestIngressIP($"'*'", GetLiveIPAddresses().FirstOrDefault()); + } + + + private static IEnumerable GetLiveIPAddresses(AddressFamily? family = null) + { + return from ni in NetworkInterface.GetAllNetworkInterfaces() + where ni.OperationalStatus == OperationalStatus.Up + let prop = ni.GetIPProperties() + from unicast in prop.UnicastAddresses + let addr = unicast.Address + where addr != IPAddress.IPv6Loopback && (family == null || addr.AddressFamily == family) + select addr; + } + + private Task TestIngressIP(string ipSetting, params IPAddress[] mustAnswer) => TestIngressIP(ipSetting, mustAnswer, Enumerable.Empty()); + private async Task TestIngressIP(string ipSetting, IEnumerable mustAnswer, IEnumerable mustFail) + { +#if !DEBUG + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; //disables running this test on windows as it stucks the test runner on the firewall open prompt +#endif + if (!mustAnswer.Any() && !mustFail.Any()) + return; // no IP to test against + + using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress"); + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-ip_test.yaml")); + File.WriteAllText(projectFile.FullName, File.ReadAllText(projectFile.FullName).Replace("__TEST_IP_STRING__", ipSetting)); + 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 = true + }; + + var client = new HttpClient(new RetryHandler(handler)); + + await RunHostingApplication(application, new HostOptions(), async (app, uri) => + { + foreach (var ip in mustAnswer.Concat(mustFail)) + { + try + { + var ingressUri = await GetServiceUrl(client, uri, "ingress"); + var reqUri = new UriBuilder(ingressUri + "/index.html") + { + Host = ip.ToString() + }; + + var htmlRequest = new HttpRequestMessage(HttpMethod.Get, reqUri.Uri); + htmlRequest.Headers.Host = "ui.example.com"; + + var htmlResponse = await client.SendAsync(htmlRequest); + htmlResponse.EnsureSuccessStatusCode(); + } + catch (Exception) when (mustFail.Contains(ip)) + { + // this is an expected failure + } + } + }); + } + [ConditionalFact] [SkipIfDockerNotRunning] public async Task NginxIngressTest() diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/tye-ip_test.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/tye-ip_test.yaml new file mode 100644 index 00000000..4e7cd599 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/tye-ip_test.yaml @@ -0,0 +1,20 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +# +# when you've given us a try, we'd love to know what you think: +# https://aka.ms/AA7q20u +# +name: apps-with-ingress-allip-ui +ingress: + - name: ingress + bindings: + - port: 8080 + ip: __TEST_IP_STRING__ + rules: + - host: ui.example.com + service: appC-ui + +services: + - name: appC-ui + project: ApplicationC-UI/ApplicationC-UI.csproj + replicas: 2 \ No newline at end of file