From dda6cdab2abe71c5c0a5c8806a5a518a5656a7cf Mon Sep 17 00:00:00 2001 From: chris van de steeg Date: Fri, 7 Oct 2022 10:58:30 +0200 Subject: [PATCH 1/7] Merge ingresses with the same name or binding into 1 ingress service --- src/tye/ApplicationBuilderExtensions.cs | 38 +++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 6f1ff5c45..227eeef82 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -193,32 +193,40 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati // Ingress get turned into services for hosting foreach (var ingress in application.Ingress) { - var rules = new List(); - - foreach (var rule in ingress.Rules) + services.TryGetValue(ingress.Name, out Service? service); + var description = service?.Description ?? new ServiceDescription(ingress.Name, new IngressRunInfo(new List())); + if (!(description.RunInfo is IngressRunInfo runinfo)) { - rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service!, rule.PreservePath)); + throw new InvalidOperationException($"Service '{ingress.Name}' was already added but not as an ingress service."); } - var runInfo = new IngressRunInfo(rules); + description.Replicas = Math.Max(ingress.Replicas, description.Replicas); - var description = new ServiceDescription(ingress.Name, runInfo) + foreach (var rule in ingress.Rules) { - Replicas = ingress.Replicas, - }; + runinfo.Rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service!, rule.PreservePath)); + } foreach (var binding in ingress.Bindings) { - description.Bindings.Add(new ServiceBinding() + //Match on name or all other properties to combind bindings to 1 instance so we can have http://*.80 listen to more then 1 service from different include files + if (!description.Bindings.Any(b => (!string.IsNullOrEmpty(b.Name) && b.Name == binding.Name) || + (b.Port == binding.Port && b.Protocol == binding.Protocol && b.IPAddress == binding.IPAddress))) { - Name = binding.Name, - Port = binding.Port, - Protocol = binding.Protocol, - IPAddress = binding.IPAddress, - }); + description.Bindings.Add(new ServiceBinding() + { + Name = binding.Name, + Port = binding.Port, + Protocol = binding.Protocol, + IPAddress = binding.IPAddress, + }); + } } - services.Add(ingress.Name, new Service(description, ServiceSource.Host)); + if (service == null) + { + services.Add(ingress.Name, new Service(description, ServiceSource.Host)); + } } return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine) From e5cd260c6b2582d8343a66965fc9accf67c979a6 Mon Sep 17 00:00:00 2001 From: chris van de steeg Date: Mon, 28 Nov 2022 11:13:08 +0100 Subject: [PATCH 2/7] Add the ingress urls to the index-view --- .../Dashboard/Pages/Index.razor | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor index cf06ba18b..7956075d0 100644 --- a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor +++ b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor @@ -51,30 +51,17 @@ } - @if (service.Description.Bindings.Any()) + @if (GetUrls(service) is IEnumerable urls && urls.Any()) { - foreach (var b in service.Description.Bindings) + foreach (var url in urls) { - if (b.Port != null) + if (url.StartsWith("http://") || url.StartsWith("https://")) { - if (b.Protocol == "http" || b.Protocol == "https") - { - var url = GetUrl(b); - @url - foreach (var r in b.Routes) - { - var routeUrl = url + r; - @routeUrl - } - } - else - { - @GetUrl(b) - } + @url } else { - @b.ConnectionString + @url } } } @@ -101,17 +88,74 @@ @code { private List _subscriptions = new List(); + private List _ingressDescriptions = new List(); + private const string INGRESS_NAME = "INGRESS"; string GetUrl(ServiceBinding b) { return $"{(b.Protocol ?? "tcp")}://{b.Host ?? "localhost"}:{b.Port}"; } + IEnumerable GetUrls(Service service) + { + foreach (var binding in service.Description.Bindings) + { + if (binding.Port != null) + { + var url = GetUrl(binding); + yield return url; + foreach (var r in binding.Routes) + { + yield return url + r; + } + } + else if (!string.IsNullOrEmpty(binding.ConnectionString)) + { + yield return binding.ConnectionString; + } + } + + foreach (ServiceDescription description in _ingressDescriptions) + { + foreach (IngressRule rule in (description?.RunInfo as IngressRunInfo)?.Rules ?? Enumerable.Empty()) + { + if (rule.Service == service.Description.Name) + { + ServiceBinding? mainBinding = description?.Bindings.FirstOrDefault(b => b.Protocol == "http") ?? + description?.Bindings.FirstOrDefault(b => b.Protocol == "https"); + + string? host = rule.Host; + if (string.IsNullOrEmpty(host)) + { + host = mainBinding?.Host ?? mainBinding?.IPAddress ?? "localhost"; + } + int port = mainBinding?.Port ?? 80; + + string url = GetUrl(new ServiceBinding + { + Port = port, + Host = host, + Protocol = mainBinding?.Protocol ?? "http" + }); + if (!string.IsNullOrEmpty(rule.Path)) + { + url += rule.Path; + } + yield return url; + } + } + } + } + protected override void OnInitialized() { foreach (var a in application.Services.Values) { _subscriptions.Add(a.ReplicaEvents.Subscribe(OnReplicaChanged)); + if (a.Description.RunInfo is IngressRunInfo) + { + _ingressDescriptions.Add(a.Description); + } } } From 47616b2e0e0059a89091cc9eaf8a3e81ba928f8f Mon Sep 17 00:00:00 2001 From: chris van de steeg Date: Fri, 23 Dec 2022 09:08:06 +0100 Subject: [PATCH 3/7] Added unittest --- test/E2ETest/Microsoft.Tye.E2ETests.csproj | 1 + test/E2ETest/TyeRunTests.cs | 114 ++++++++++++++++++ .../apps-with-ingress/ApplicationA/tye.yaml | 24 ++++ .../apps-with-ingress/ApplicationB/tye.yaml | 21 ++++ .../apps-with-ingress/tye-mergeingress.yaml | 13 ++ 5 files changed, 173 insertions(+) create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress.yaml diff --git a/test/E2ETest/Microsoft.Tye.E2ETests.csproj b/test/E2ETest/Microsoft.Tye.E2ETests.csproj index 552d54f0b..843e39606 100644 --- a/test/E2ETest/Microsoft.Tye.E2ETests.csproj +++ b/test/E2ETest/Microsoft.Tye.E2ETests.csproj @@ -32,6 +32,7 @@ + diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 22047301a..60fd7b9d0 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -771,6 +771,120 @@ public async Task IngressQueryAndContentProxyingTest() }); } + [Fact] + public async Task MergedIngressRunTest() + { + using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress.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)); + var wsClient = new ClientWebSocket(); + + await RunHostingApplication(application, new HostOptions(), async (app, uri) => + { + 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"); + + Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync()); + Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync()); + + var requestA = new HttpRequestMessage(HttpMethod.Get, ingressUri); + requestA.Headers.Host = "a.example.com"; + var requestB = new HttpRequestMessage(HttpMethod.Get, ingressUri); + requestB.Headers.Host = "b.example.com"; + + responseA = await client.SendAsync(requestA); + responseB = await client.SendAsync(requestB); + + Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync()); + Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync()); + + // checking preservePath behavior + var responsePreservePath = await client.GetAsync(ingressUri + "/C/test"); + Assert.Contains("Hit path /C/test", await responsePreservePath.Content.ReadAsStringAsync()); + + string GetWebSocketUri(string uri) + { + if (uri.StartsWith("http")) + { + return "ws" + uri.Substring(4); + } + else if (uri.StartsWith("https")) + { + return "wss" + uri.Substring(5); + } + + throw new NotSupportedException(); + } + + // Check the websocket endpoint + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var wsUri = GetWebSocketUri(ingressUri); + await wsClient.ConnectAsync(new Uri(wsUri + "/A/ws"), cts.Token); + var data = Encoding.UTF8.GetBytes("Hello World"); + await wsClient.SendAsync(data, WebSocketMessageType.Text, endOfMessage: true, cts.Token); + var receiveBuffer = new byte[4096]; + var result = await wsClient.ReceiveAsync(receiveBuffer.AsMemory(), cts.Token); + Assert.True(result.EndOfMessage); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + Assert.Equal(data.Length, result.Count); + Assert.Equal(data, receiveBuffer.AsMemory(0, result.Count).ToArray()); + await wsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cts.Token); + }); + } + + [Fact] + public async Task MergedIngressQueryAndContentProxyingTest() + { + using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress.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, new HostOptions(), async (app, uri) => + { + var ingressUri = await GetServiceUrl(client, uri, "ingress"); + + var request = new HttpRequestMessage(HttpMethod.Post, ingressUri + "/A/data?key1=value1&key2=value2"); + request.Content = new StringContent("some content"); + + var response = await client.SendAsync(request); + var responseContent = + JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); + + Assert.Equal("some content", responseContent!["content"]); + Assert.Equal("?key1=value1&key2=value2", responseContent["query"]); + }); + } + [Fact] public async Task IngressStaticFilesTest() { diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye.yaml new file mode 100644 index 000000000..bdab44409 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye.yaml @@ -0,0 +1,24 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + rules: + - path: /A + service: appA + - path: /C + service: appA + preservePath: true + - host: a.example.com + service: appA + +services: +- name: appA + project: ./ApplicationA.csproj + replicas: 2 diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye.yaml new file mode 100644 index 000000000..3dbc88827 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye.yaml @@ -0,0 +1,21 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + rules: + - path: /B + service: appB + - host: b.example.com + service: appB + +services: +- name: appB + project: ./ApplicationB.csproj + replicas: 2 \ No newline at end of file diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress.yaml new file mode 100644 index 000000000..efcc2a110 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress.yaml @@ -0,0 +1,13 @@ +# 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 + +services: +- name: appA + include: ApplicationA/tye.yaml +- name: appB + include: ApplicationB/tye.yaml From 588a18bf9629e02db734a80fe4d9a6b7cb568767 Mon Sep 17 00:00:00 2001 From: chris van de steeg Date: Fri, 23 Dec 2022 09:08:18 +0100 Subject: [PATCH 4/7] Throw exception on conflict situation --- src/tye/ApplicationBuilderExtensions.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 227eeef82..435ff46b2 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -210,8 +210,24 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati foreach (var binding in ingress.Bindings) { //Match on name or all other properties to combind bindings to 1 instance so we can have http://*.80 listen to more then 1 service from different include files - if (!description.Bindings.Any(b => (!string.IsNullOrEmpty(b.Name) && b.Name == binding.Name) || - (b.Port == binding.Port && b.Protocol == binding.Protocol && b.IPAddress == binding.IPAddress))) + var existing = description.Bindings.FirstOrDefault(b => !string.IsNullOrEmpty(b.Name) && b.Name == binding.Name); + if (existing != null) + { + //if we're using an existing binding based on name, the other properties should match + if (existing.Port != binding.Port || existing.Protocol != binding.Protocol || existing.IPAddress != binding.IPAddress) + { + throw new InvalidOperationException($"Ingress {ingress.Name} already has a binding with name {binding.Name} but with different settings."); + } + } + else + { + existing = description.Bindings.FirstOrDefault(b => b.Port == binding.Port && b.Protocol == binding.Protocol && b.IPAddress == binding.IPAddress); + if (existing != null && existing.Name != binding.Name) + { + throw new InvalidOperationException($"Ingress {ingress.Name} already has a binding for {binding.Protocol}://{binding.IPAddress}:{binding.Port} but with a different name."); + } + } + if (existing == null) { description.Bindings.Add(new ServiceBinding() { From 34bbdff30f0ccaa6840269a63ee4ae87334d3a91 Mon Sep 17 00:00:00 2001 From: chris van de steeg Date: Fri, 23 Dec 2022 10:19:50 +0100 Subject: [PATCH 5/7] Added more conflict stituations --- src/Microsoft.Tye.Core/IngressBindingBuilder.cs | 8 ++++++++ src/tye/ApplicationBuilderExtensions.cs | 17 +++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Tye.Core/IngressBindingBuilder.cs b/src/Microsoft.Tye.Core/IngressBindingBuilder.cs index 6153b3423..eccc6742a 100644 --- a/src/Microsoft.Tye.Core/IngressBindingBuilder.cs +++ b/src/Microsoft.Tye.Core/IngressBindingBuilder.cs @@ -10,5 +10,13 @@ public sealed class IngressBindingBuilder public int? Port { get; set; } public string? Protocol { get; set; } // HTTP or HTTPS public string? IPAddress { get; set; } + + public override string ToString() + { + return (string.IsNullOrEmpty(Name) ? "" : "[" + Name + "] -> ") + + (string.IsNullOrEmpty(Protocol) ? "" : Protocol + "://") + + (string.IsNullOrEmpty(IPAddress) ? "*" : IPAddress) + + (Port == null || Port == 0 ? "" : ":" + Port); + } } } diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 435ff46b2..37fc97221 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -197,34 +197,39 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati var description = service?.Description ?? new ServiceDescription(ingress.Name, new IngressRunInfo(new List())); if (!(description.RunInfo is IngressRunInfo runinfo)) { - throw new InvalidOperationException($"Service '{ingress.Name}' was already added but not as an ingress service."); + throw new CommandException($"Service '{ingress.Name}' was already added but not as an ingress service."); } description.Replicas = Math.Max(ingress.Replicas, description.Replicas); foreach (var rule in ingress.Rules) { + if (runinfo.Rules.FirstOrDefault(r => (r.Host ?? String.Empty).Equals(rule.Host ?? String.Empty, StringComparison.InvariantCultureIgnoreCase) + && (r.Path ?? String.Empty).Equals(rule.Path ?? String.Empty, StringComparison.InvariantCultureIgnoreCase)) + is IngressRule existing) + { + throw new CommandException($"Cannot add rule for service {rule.Service} to ingress '{ingress.Name}', it already has a rule for service {existing.Service} with Host {rule.Host} and Path {rule.Path}."); + } runinfo.Rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service!, rule.PreservePath)); } foreach (var binding in ingress.Bindings) { - //Match on name or all other properties to combind bindings to 1 instance so we can have http://*.80 listen to more then 1 service from different include files var existing = description.Bindings.FirstOrDefault(b => !string.IsNullOrEmpty(b.Name) && b.Name == binding.Name); if (existing != null) { //if we're using an existing binding based on name, the other properties should match if (existing.Port != binding.Port || existing.Protocol != binding.Protocol || existing.IPAddress != binding.IPAddress) { - throw new InvalidOperationException($"Ingress {ingress.Name} already has a binding with name {binding.Name} but with different settings."); + throw new CommandException($"Ingress {ingress.Name} already has a binding with name {binding.Name} but with different settings."); } } else { - existing = description.Bindings.FirstOrDefault(b => b.Port == binding.Port && b.Protocol == binding.Protocol && b.IPAddress == binding.IPAddress); - if (existing != null && existing.Name != binding.Name) + existing = description.Bindings.FirstOrDefault(b => b.Port == binding.Port && (b.Protocol ?? "http") == binding.Protocol && b.IPAddress == binding.IPAddress); + if (existing != null && !(existing.Name ?? string.Empty).Equals(binding.Name ?? string.Empty, StringComparison.InvariantCultureIgnoreCase)) { - throw new InvalidOperationException($"Ingress {ingress.Name} already has a binding for {binding.Protocol}://{binding.IPAddress}:{binding.Port} but with a different name."); + throw new CommandException($"Ingress {ingress.Name} already has a binding with the same ipaddress and/or port, {binding} cannot be added."); } } if (existing == null) From 1fab00cd54279bce16d311cabe142cdda0152101 Mon Sep 17 00:00:00 2001 From: chris van de steeg Date: Fri, 23 Dec 2022 10:20:04 +0100 Subject: [PATCH 6/7] Add conflict stituation unittests --- test/E2ETest/Microsoft.Tye.E2ETests.csproj | 9 + test/E2ETest/TyeRunTests.cs | 278 ++++++++++-------- .../ApplicationA/tye-bindingconflict.yaml | 25 ++ .../ApplicationA/tye-nameconflict.yaml | 26 ++ .../ApplicationA/tye-ruleconflict.yaml | 25 ++ .../ApplicationB/tye-bindingconflict.yaml | 22 ++ .../ApplicationB/tye-nameconflict.yaml | 22 ++ .../ApplicationB/tye-ruleconflict.yaml | 22 ++ .../tye-mergeingress-bindingconflict.yaml | 13 + .../tye-mergeingress-nameconflict.yaml | 13 + .../tye-mergeingress-ruleconflict.yaml | 13 + 11 files changed, 353 insertions(+), 115 deletions(-) create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-bindingconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-nameconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-ruleconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-bindingconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-nameconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-ruleconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-bindingconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-nameconflict.yaml create mode 100644 test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-ruleconflict.yaml diff --git a/test/E2ETest/Microsoft.Tye.E2ETests.csproj b/test/E2ETest/Microsoft.Tye.E2ETests.csproj index 843e39606..341e4e139 100644 --- a/test/E2ETest/Microsoft.Tye.E2ETests.csproj +++ b/test/E2ETest/Microsoft.Tye.E2ETests.csproj @@ -32,7 +32,16 @@ + + + + + + + + + diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 60fd7b9d0..defd0f7c9 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.CommandLine.IO; +using System.Data; using System.IO; using System.Linq; using System.Net; @@ -18,7 +19,6 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Primitives; using Microsoft.Tye; using Microsoft.Tye.Hosting; using Microsoft.Tye.Hosting.Model; @@ -771,120 +771,6 @@ public async Task IngressQueryAndContentProxyingTest() }); } - [Fact] - public async Task MergedIngressRunTest() - { - using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress"); - - var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress.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)); - var wsClient = new ClientWebSocket(); - - await RunHostingApplication(application, new HostOptions(), async (app, uri) => - { - 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"); - - Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync()); - Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync()); - - var requestA = new HttpRequestMessage(HttpMethod.Get, ingressUri); - requestA.Headers.Host = "a.example.com"; - var requestB = new HttpRequestMessage(HttpMethod.Get, ingressUri); - requestB.Headers.Host = "b.example.com"; - - responseA = await client.SendAsync(requestA); - responseB = await client.SendAsync(requestB); - - Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync()); - Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync()); - - // checking preservePath behavior - var responsePreservePath = await client.GetAsync(ingressUri + "/C/test"); - Assert.Contains("Hit path /C/test", await responsePreservePath.Content.ReadAsStringAsync()); - - string GetWebSocketUri(string uri) - { - if (uri.StartsWith("http")) - { - return "ws" + uri.Substring(4); - } - else if (uri.StartsWith("https")) - { - return "wss" + uri.Substring(5); - } - - throw new NotSupportedException(); - } - - // Check the websocket endpoint - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - var wsUri = GetWebSocketUri(ingressUri); - await wsClient.ConnectAsync(new Uri(wsUri + "/A/ws"), cts.Token); - var data = Encoding.UTF8.GetBytes("Hello World"); - await wsClient.SendAsync(data, WebSocketMessageType.Text, endOfMessage: true, cts.Token); - var receiveBuffer = new byte[4096]; - var result = await wsClient.ReceiveAsync(receiveBuffer.AsMemory(), cts.Token); - Assert.True(result.EndOfMessage); - Assert.Equal(WebSocketMessageType.Text, result.MessageType); - Assert.Equal(data.Length, result.Count); - Assert.Equal(data, receiveBuffer.AsMemory(0, result.Count).ToArray()); - await wsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cts.Token); - }); - } - - [Fact] - public async Task MergedIngressQueryAndContentProxyingTest() - { - using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress"); - - var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress.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, new HostOptions(), async (app, uri) => - { - var ingressUri = await GetServiceUrl(client, uri, "ingress"); - - var request = new HttpRequestMessage(HttpMethod.Post, ingressUri + "/A/data?key1=value1&key2=value2"); - request.Content = new StringContent("some content"); - - var response = await client.SendAsync(request); - var responseContent = - JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); - - Assert.Equal("some content", responseContent!["content"]); - Assert.Equal("?key1=value1&key2=value2", responseContent["query"]); - }); - } - [Fact] public async Task IngressStaticFilesTest() { @@ -1458,6 +1344,168 @@ public async Task RunWithDotnetEnvVarsDoesNotGetOverriddenByDefaultDotnetEnvVars }); } + [Fact] + public async Task MergedIngressRunTest() + { + using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress.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)); + var wsClient = new ClientWebSocket(); + + await RunHostingApplication(application, new HostOptions(), async (app, uri) => + { + 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"); + + Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync()); + Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync()); + + var requestA = new HttpRequestMessage(HttpMethod.Get, ingressUri); + requestA.Headers.Host = "a.example.com"; + var requestB = new HttpRequestMessage(HttpMethod.Get, ingressUri); + requestB.Headers.Host = "b.example.com"; + + responseA = await client.SendAsync(requestA); + responseB = await client.SendAsync(requestB); + + Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync()); + Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync()); + + // checking preservePath behavior + var responsePreservePath = await client.GetAsync(ingressUri + "/C/test"); + Assert.Contains("Hit path /C/test", await responsePreservePath.Content.ReadAsStringAsync()); + + string GetWebSocketUri(string uri) + { + if (uri.StartsWith("http")) + { + return "ws" + uri.Substring(4); + } + else if (uri.StartsWith("https")) + { + return "wss" + uri.Substring(5); + } + + throw new NotSupportedException(); + } + + // Check the websocket endpoint + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var wsUri = GetWebSocketUri(ingressUri); + await wsClient.ConnectAsync(new Uri(wsUri + "/A/ws"), cts.Token); + var data = Encoding.UTF8.GetBytes("Hello World"); + await wsClient.SendAsync(data, WebSocketMessageType.Text, endOfMessage: true, cts.Token); + var receiveBuffer = new byte[4096]; + var result = await wsClient.ReceiveAsync(receiveBuffer.AsMemory(), cts.Token); + Assert.True(result.EndOfMessage); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + Assert.Equal(data.Length, result.Count); + Assert.Equal(data, receiveBuffer.AsMemory(0, result.Count).ToArray()); + await wsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cts.Token); + }); + } + + [Fact] + public async Task MergedIngressQueryAndContentProxyingTest() + { + using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress.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, new HostOptions(), async (app, uri) => + { + var ingressUri = await GetServiceUrl(client, uri, "ingress"); + + var request = new HttpRequestMessage(HttpMethod.Post, ingressUri + "/A/data?key1=value1&key2=value2"); + request.Content = new StringContent("some content"); + + var response = await client.SendAsync(request); + var responseContent = + JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); + + Assert.Equal("some content", responseContent!["content"]); + Assert.Equal("?key1=value1&key2=value2", responseContent["query"]); + }); + } + + [Fact] + public async Task IngressMergeConflictOnNameProducesCorrectErrorMessage() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory("apps-with-ingress"); + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress-nameconflict.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + var exception = Assert.Throws(() => application.ToHostingApplication()); + + var ingressName = "ingress"; + var bindingName = "example"; + Assert.Equal($"Ingress {ingressName} already has a binding with name {bindingName} but with different settings.", exception.Message); + } + + [Fact] + public async Task IngressMergeConflictOnRuleProducesCorrectErrorMessage() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory("apps-with-ingress"); + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress-ruleconflict.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + var exception = Assert.Throws(() => application.ToHostingApplication()); + + var ingressName = "ingress"; + var ruleService = "appB".ToLowerInvariant(); + var existingService = "appA".ToLowerInvariant(); + var ruleHost = ""; + var rulePath = "/A"; + Assert.Equal($"Cannot add rule for service {ruleService} to ingress '{ingressName}', it already has a rule for service {existingService} with Host {ruleHost} and Path {rulePath}.", exception.Message); + } + + [Fact] + public async Task IngressMergeConflictOnBindingProducesCorrectErrorMessage() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory("apps-with-ingress"); + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-mergeingress-bindingconflict.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + var exception = Assert.Throws(() => application.ToHostingApplication()); + + var ingressName = "ingress"; + var binding = new IngressBindingBuilder { Port = 8080, Name = "example2", Protocol = "http" }; + Assert.Equal($"Ingress {ingressName} already has a binding with the same ipaddress and/or port, {binding} cannot be added.", exception.Message); + } + private async Task GetServiceUrl(HttpClient client, Uri uri, string serviceName) { var serviceResult = await client.GetStringAsync($"{uri}api/v1/services/{serviceName}"); diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-bindingconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-bindingconflict.yaml new file mode 100644 index 000000000..752d6bb97 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-bindingconflict.yaml @@ -0,0 +1,25 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + name: example + rules: + - path: /A + service: appA + - path: /C + service: appA + preservePath: true + - host: a.example.com + service: appA + +services: +- name: appA + project: ./ApplicationA.csproj + replicas: 2 diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-nameconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-nameconflict.yaml new file mode 100644 index 000000000..3b6f11494 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-nameconflict.yaml @@ -0,0 +1,26 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + name: example + ip: 192.168.1.1 + rules: + - path: /A + service: appA + - path: /C + service: appA + preservePath: true + - host: a.example.com + service: appA + +services: +- name: appA + project: ./ApplicationA.csproj + replicas: 2 diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-ruleconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-ruleconflict.yaml new file mode 100644 index 000000000..752d6bb97 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationA/tye-ruleconflict.yaml @@ -0,0 +1,25 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + name: example + rules: + - path: /A + service: appA + - path: /C + service: appA + preservePath: true + - host: a.example.com + service: appA + +services: +- name: appA + project: ./ApplicationA.csproj + replicas: 2 diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-bindingconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-bindingconflict.yaml new file mode 100644 index 000000000..2840aba2e --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-bindingconflict.yaml @@ -0,0 +1,22 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + name: example2 + rules: + - path: /B + service: appB + - host: b.example.com + service: appB + +services: +- name: appB + project: ./ApplicationB.csproj + replicas: 2 \ No newline at end of file diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-nameconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-nameconflict.yaml new file mode 100644 index 000000000..95ed72651 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-nameconflict.yaml @@ -0,0 +1,22 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + name: example + rules: + - path: /B + service: appB + - host: b.example.com + service: appB + +services: +- name: appB + project: ./ApplicationB.csproj + replicas: 2 \ No newline at end of file diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-ruleconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-ruleconflict.yaml new file mode 100644 index 000000000..3eda92b32 --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/ApplicationB/tye-ruleconflict.yaml @@ -0,0 +1,22 @@ +# 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 +ingress: + - name: ingress + bindings: + - port: 8080 + name: example + rules: + - path: /A + service: appB + - host: b.example.com + service: appB + +services: +- name: appB + project: ./ApplicationB.csproj + replicas: 2 \ No newline at end of file diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-bindingconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-bindingconflict.yaml new file mode 100644 index 000000000..b4bee743d --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-bindingconflict.yaml @@ -0,0 +1,13 @@ +# 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 + +services: +- name: appA + include: ApplicationA/tye-bindingconflict.yaml +- name: appB + include: ApplicationB/tye-bindingconflict.yaml diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-nameconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-nameconflict.yaml new file mode 100644 index 000000000..b657eee2b --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-nameconflict.yaml @@ -0,0 +1,13 @@ +# 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 + +services: +- name: appA + include: ApplicationA/tye-nameconflict.yaml +- name: appB + include: ApplicationB/tye-nameconflict.yaml diff --git a/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-ruleconflict.yaml b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-ruleconflict.yaml new file mode 100644 index 000000000..52434a78d --- /dev/null +++ b/test/E2ETest/testassets/projects/apps-with-ingress/tye-mergeingress-ruleconflict.yaml @@ -0,0 +1,13 @@ +# 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 + +services: +- name: appA + include: ApplicationA/tye-ruleconflict.yaml +- name: appB + include: ApplicationB/tye-ruleconflict.yaml From 65e01439f32ea46f32705e579efe8d9c6a868f7d Mon Sep 17 00:00:00 2001 From: chris van de steeg Date: Fri, 23 Dec 2022 12:18:02 +0100 Subject: [PATCH 7/7] Fix whitespaces --- src/tye/ApplicationBuilderExtensions.cs | 2 +- test/E2ETest/TyeRunTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 37fc97221..55e81cf77 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -223,7 +223,7 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati { throw new CommandException($"Ingress {ingress.Name} already has a binding with name {binding.Name} but with different settings."); } - } + } else { existing = description.Bindings.FirstOrDefault(b => b.Port == binding.Port && (b.Protocol ?? "http") == binding.Protocol && b.IPAddress == binding.IPAddress); diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 785a6e196..e6568b7b2 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -1468,7 +1468,7 @@ public async Task IngressMergeConflictOnNameProducesCorrectErrorMessage() var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var exception = Assert.Throws(() => application.ToHostingApplication()); - + var ingressName = "ingress"; var bindingName = "example"; Assert.Equal($"Ingress {ingressName} already has a binding with name {bindingName} but with different settings.", exception.Message);