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/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);
+ }
}
}
diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs
index 6f1ff5c45..55e81cf77 100644
--- a/src/tye/ApplicationBuilderExtensions.cs
+++ b/src/tye/ApplicationBuilderExtensions.cs
@@ -193,32 +193,61 @@ 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 CommandException($"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,
- };
+ 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)
{
- description.Bindings.Add(new ServiceBinding()
+ var existing = description.Bindings.FirstOrDefault(b => !string.IsNullOrEmpty(b.Name) && b.Name == binding.Name);
+ if (existing != null)
{
- Name = binding.Name,
- Port = binding.Port,
- Protocol = binding.Protocol,
- IPAddress = binding.IPAddress,
- });
+ //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 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);
+ if (existing != null && !(existing.Name ?? string.Empty).Equals(binding.Name ?? string.Empty, StringComparison.InvariantCultureIgnoreCase))
+ {
+ throw new CommandException($"Ingress {ingress.Name} already has a binding with the same ipaddress and/or port, {binding} cannot be added.");
+ }
+ }
+ if (existing == null)
+ {
+ 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)
diff --git a/test/E2ETest/Microsoft.Tye.E2ETests.csproj b/test/E2ETest/Microsoft.Tye.E2ETests.csproj
index 552d54f0b..341e4e139 100644
--- a/test/E2ETest/Microsoft.Tye.E2ETests.csproj
+++ b/test/E2ETest/Microsoft.Tye.E2ETests.csproj
@@ -32,6 +32,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs
index 09c09434e..e6568b7b2 100644
--- a/test/E2ETest/TyeRunTests.cs
+++ b/test/E2ETest/TyeRunTests.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.CommandLine.IO;
+using System.Data;
using System.IO;
using System.Linq;
using System.Net;
@@ -19,7 +20,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;
@@ -1345,6 +1345,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/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-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/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-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
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
|