mirror of https://github.com/dotnet/tye.git
Browse Source
* Added ingress support in local orchestrator - Supports host and port mapping to other services - Added sample to show ingress usage - Added a testpull/181/head
committed by
GitHub
34 changed files with 1279 additions and 9 deletions
@ -0,0 +1,7 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.1</TargetFramework> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
|
||||
|
namespace ApplicationA |
||||
|
{ |
||||
|
public class Program |
||||
|
{ |
||||
|
public static void Main(string[] args) |
||||
|
{ |
||||
|
CreateHostBuilder(args).Build().Run(); |
||||
|
} |
||||
|
|
||||
|
public static IHostBuilder CreateHostBuilder(string[] args) => |
||||
|
Host.CreateDefaultBuilder(args) |
||||
|
.ConfigureWebHostDefaults(webBuilder => |
||||
|
{ |
||||
|
webBuilder.UseStartup<Startup>(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
{ |
||||
|
"iisSettings": { |
||||
|
"windowsAuthentication": false, |
||||
|
"anonymousAuthentication": true, |
||||
|
"iisExpress": { |
||||
|
"applicationUrl": "http://localhost:2755", |
||||
|
"sslPort": 44369 |
||||
|
} |
||||
|
}, |
||||
|
"profiles": { |
||||
|
"IIS Express": { |
||||
|
"commandName": "IISExpress", |
||||
|
"launchBrowser": true, |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
} |
||||
|
}, |
||||
|
"ApplicationA": { |
||||
|
"commandName": "Project", |
||||
|
"launchBrowser": true, |
||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000", |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
|
||||
|
namespace ApplicationA |
||||
|
{ |
||||
|
public class Startup |
||||
|
{ |
||||
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
|
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
|
public void ConfigureServices(IServiceCollection services) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
||||
|
{ |
||||
|
if (env.IsDevelopment()) |
||||
|
{ |
||||
|
app.UseDeveloperExceptionPage(); |
||||
|
} |
||||
|
|
||||
|
app.UseRouting(); |
||||
|
|
||||
|
app.UseEndpoints(endpoints => |
||||
|
{ |
||||
|
endpoints.MapGet("/", async context => |
||||
|
{ |
||||
|
await context.Response.WriteAsync("Hello from Application A " + Environment.GetEnvironmentVariable("APP_INSTANCE") ?? Environment.GetEnvironmentVariable("HOSTNAME")); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"Logging": { |
||||
|
"LogLevel": { |
||||
|
"Default": "Information", |
||||
|
"Microsoft": "Warning", |
||||
|
"Microsoft.Hosting.Lifetime": "Information" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"Logging": { |
||||
|
"LogLevel": { |
||||
|
"Default": "Information", |
||||
|
"Microsoft": "Warning", |
||||
|
"Microsoft.Hosting.Lifetime": "Information" |
||||
|
} |
||||
|
}, |
||||
|
"AllowedHosts": "*" |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.1</TargetFramework> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
|
||||
|
namespace ApplicationB |
||||
|
{ |
||||
|
public class Program |
||||
|
{ |
||||
|
public static void Main(string[] args) |
||||
|
{ |
||||
|
CreateHostBuilder(args).Build().Run(); |
||||
|
} |
||||
|
|
||||
|
public static IHostBuilder CreateHostBuilder(string[] args) => |
||||
|
Host.CreateDefaultBuilder(args) |
||||
|
.ConfigureWebHostDefaults(webBuilder => |
||||
|
{ |
||||
|
webBuilder.UseStartup<Startup>(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
{ |
||||
|
"iisSettings": { |
||||
|
"windowsAuthentication": false, |
||||
|
"anonymousAuthentication": true, |
||||
|
"iisExpress": { |
||||
|
"applicationUrl": "http://localhost:19251", |
||||
|
"sslPort": 44343 |
||||
|
} |
||||
|
}, |
||||
|
"profiles": { |
||||
|
"IIS Express": { |
||||
|
"commandName": "IISExpress", |
||||
|
"launchBrowser": true, |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
} |
||||
|
}, |
||||
|
"ApplicationB": { |
||||
|
"commandName": "Project", |
||||
|
"launchBrowser": true, |
||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000", |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
|
||||
|
namespace ApplicationB |
||||
|
{ |
||||
|
public class Startup |
||||
|
{ |
||||
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
|
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
|
public void ConfigureServices(IServiceCollection services) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
||||
|
{ |
||||
|
if (env.IsDevelopment()) |
||||
|
{ |
||||
|
app.UseDeveloperExceptionPage(); |
||||
|
} |
||||
|
|
||||
|
app.UseRouting(); |
||||
|
|
||||
|
app.UseEndpoints(endpoints => |
||||
|
{ |
||||
|
endpoints.MapGet("/", async context => |
||||
|
{ |
||||
|
await context.Response.WriteAsync("Hello from Application B " + Environment.GetEnvironmentVariable("APP_INSTANCE") ?? Environment.GetEnvironmentVariable("HOSTNAME")); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"Logging": { |
||||
|
"LogLevel": { |
||||
|
"Default": "Information", |
||||
|
"Microsoft": "Warning", |
||||
|
"Microsoft.Hosting.Lifetime": "Information" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"Logging": { |
||||
|
"LogLevel": { |
||||
|
"Default": "Information", |
||||
|
"Microsoft": "Warning", |
||||
|
"Microsoft.Hosting.Lifetime": "Information" |
||||
|
} |
||||
|
}, |
||||
|
"AllowedHosts": "*" |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
|
||||
|
Microsoft Visual Studio Solution File, Format Version 12.00 |
||||
|
# Visual Studio 15 |
||||
|
VisualStudioVersion = 15.0.26124.0 |
||||
|
MinimumVisualStudioVersion = 15.0.26124.0 |
||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationA", "ApplicationA\ApplicationA.csproj", "{5A9DC239-55BB-4951-B081-35931BF8C867}" |
||||
|
EndProject |
||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationB", "ApplicationB\ApplicationB.csproj", "{AE1F10D3-BFAE-4D23-ADCF-06770237285D}" |
||||
|
EndProject |
||||
|
Global |
||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution |
||||
|
Debug|Any CPU = Debug|Any CPU |
||||
|
Debug|x64 = Debug|x64 |
||||
|
Debug|x86 = Debug|x86 |
||||
|
Release|Any CPU = Release|Any CPU |
||||
|
Release|x64 = Release|x64 |
||||
|
Release|x86 = Release|x86 |
||||
|
EndGlobalSection |
||||
|
GlobalSection(SolutionProperties) = preSolution |
||||
|
HideSolutionNode = FALSE |
||||
|
EndGlobalSection |
||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|Any CPU.Build.0 = Debug|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x64.ActiveCfg = Debug|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x64.Build.0 = Debug|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x86.ActiveCfg = Debug|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Debug|x86.Build.0 = Debug|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Release|Any CPU.ActiveCfg = Release|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Release|Any CPU.Build.0 = Release|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x64.ActiveCfg = Release|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x64.Build.0 = Release|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x86.ActiveCfg = Release|Any CPU |
||||
|
{5A9DC239-55BB-4951-B081-35931BF8C867}.Release|x86.Build.0 = Release|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|Any CPU.Build.0 = Debug|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x64.ActiveCfg = Debug|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x64.Build.0 = Debug|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x86.ActiveCfg = Debug|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Debug|x86.Build.0 = Debug|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|Any CPU.ActiveCfg = Release|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|Any CPU.Build.0 = Release|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x64.ActiveCfg = Release|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x64.Build.0 = Release|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x86.ActiveCfg = Release|Any CPU |
||||
|
{AE1F10D3-BFAE-4D23-ADCF-06770237285D}.Release|x86.Build.0 = Release|Any CPU |
||||
|
EndGlobalSection |
||||
|
EndGlobal |
||||
@ -0,0 +1,28 @@ |
|||||
|
# 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: /B |
||||
|
service: appB |
||||
|
- host: a.example.com |
||||
|
service: appA |
||||
|
- host: b.example.com |
||||
|
service: appB |
||||
|
|
||||
|
services: |
||||
|
- name: appA |
||||
|
project: ApplicationA/ApplicationA.csproj |
||||
|
replicas: 2 |
||||
|
- name: appB |
||||
|
project: ApplicationB/ApplicationB.csproj |
||||
|
replicas: 2 |
||||
@ -0,0 +1,209 @@ |
|||||
|
// 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.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Routing.Matching |
||||
|
{ |
||||
|
internal sealed class IngressHostMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy |
||||
|
{ |
||||
|
private const string WildcardHost = "*"; |
||||
|
private const string WildcardPrefix = "*."; |
||||
|
|
||||
|
// Run after HTTP methods, but before 'default'.
|
||||
|
public override int Order { get; } = -100; |
||||
|
|
||||
|
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints) |
||||
|
{ |
||||
|
return endpoints.Any(e => |
||||
|
{ |
||||
|
var hosts = e.Metadata.GetMetadata<IngressHostMetadata>()?.Hosts; |
||||
|
if (hosts == null || hosts.Count == 0) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
foreach (var host in hosts) |
||||
|
{ |
||||
|
// Don't run policy on endpoints that match everything
|
||||
|
var key = CreateEdgeKey(host); |
||||
|
if (!key.MatchesAll) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) |
||||
|
{ |
||||
|
if (httpContext == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(httpContext)); |
||||
|
} |
||||
|
|
||||
|
if (candidates == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(candidates)); |
||||
|
} |
||||
|
|
||||
|
for (var i = 0; i < candidates.Count; i++) |
||||
|
{ |
||||
|
if (!candidates.IsValidCandidate(i)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var hosts = candidates[i].Endpoint.Metadata.GetMetadata<IngressHostMetadata>()?.Hosts; |
||||
|
if (hosts == null || hosts.Count == 0) |
||||
|
{ |
||||
|
// Can match any host.
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var matched = false; |
||||
|
var (requestHost, requestPort) = GetHostAndPort(httpContext); |
||||
|
for (var j = 0; j < hosts.Count; j++) |
||||
|
{ |
||||
|
var host = hosts[j].AsSpan(); |
||||
|
var port = ReadOnlySpan<char>.Empty; |
||||
|
|
||||
|
// Split into host and port
|
||||
|
var pivot = host.IndexOf(':'); |
||||
|
if (pivot >= 0) |
||||
|
{ |
||||
|
port = host.Slice(pivot + 1); |
||||
|
host = host.Slice(0, pivot); |
||||
|
} |
||||
|
|
||||
|
if (host == null || MemoryExtensions.Equals(host, WildcardHost, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
// Can match any host
|
||||
|
} |
||||
|
else if ( |
||||
|
host.StartsWith(WildcardPrefix) && |
||||
|
|
||||
|
// Note that we only slice of the `*`. We want to match the leading `.` also.
|
||||
|
MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
// Matches a suffix wildcard.
|
||||
|
} |
||||
|
else if (MemoryExtensions.Equals(requestHost, host, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
// Matches exactly
|
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// If we get here then the host doesn't match.
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (MemoryExtensions.Equals(port, WildcardHost, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
// Port is a wildcard, we allow any port.
|
||||
|
} |
||||
|
else if (port.Length > 0 && (!int.TryParse(port, out var parsed) || parsed != requestPort)) |
||||
|
{ |
||||
|
// If we get here then the port doesn't match.
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
matched = true; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (!matched) |
||||
|
{ |
||||
|
candidates.SetValidity(i, false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
private static EdgeKey CreateEdgeKey(string host) |
||||
|
{ |
||||
|
if (host == null) |
||||
|
{ |
||||
|
return EdgeKey.WildcardEdgeKey; |
||||
|
} |
||||
|
|
||||
|
var hostParts = host.Split(':'); |
||||
|
if (hostParts.Length == 1) |
||||
|
{ |
||||
|
if (!string.IsNullOrEmpty(hostParts[0])) |
||||
|
{ |
||||
|
return new EdgeKey(hostParts[0], null); |
||||
|
} |
||||
|
} |
||||
|
if (hostParts.Length == 2) |
||||
|
{ |
||||
|
if (!string.IsNullOrEmpty(hostParts[0])) |
||||
|
{ |
||||
|
if (int.TryParse(hostParts[1], out var port)) |
||||
|
{ |
||||
|
return new EdgeKey(hostParts[0], port); |
||||
|
} |
||||
|
else if (string.Equals(hostParts[1], WildcardHost, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return new EdgeKey(hostParts[0], null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new InvalidOperationException($"Could not parse host: {host}"); |
||||
|
} |
||||
|
|
||||
|
private static (string host, int? port) GetHostAndPort(HttpContext httpContext) |
||||
|
{ |
||||
|
var hostString = httpContext.Request.Host; |
||||
|
if (hostString.Port != null) |
||||
|
{ |
||||
|
return (hostString.Host, hostString.Port); |
||||
|
} |
||||
|
else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
return (hostString.Host, 443); |
||||
|
} |
||||
|
else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
return (hostString.Host, 80); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return (hostString.Host, null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly struct EdgeKey |
||||
|
{ |
||||
|
internal static readonly EdgeKey WildcardEdgeKey = new EdgeKey(null, null); |
||||
|
|
||||
|
public readonly int? Port; |
||||
|
public readonly string Host; |
||||
|
|
||||
|
public EdgeKey(string? host, int? port) |
||||
|
{ |
||||
|
Host = host ?? WildcardHost; |
||||
|
Port = port; |
||||
|
|
||||
|
HasHostWildcard = Host.StartsWith(WildcardPrefix, StringComparison.Ordinal); |
||||
|
} |
||||
|
|
||||
|
public bool HasHostWildcard { get; } |
||||
|
|
||||
|
public bool MatchesHost => !string.Equals(Host, WildcardHost, StringComparison.Ordinal); |
||||
|
|
||||
|
public bool MatchesPort => Port != null; |
||||
|
|
||||
|
public bool MatchesAll => !MatchesHost && !MatchesPort; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// 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.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Routing.Matching |
||||
|
{ |
||||
|
internal class IngressHostMetadata |
||||
|
{ |
||||
|
public IngressHostMetadata(params string[] hosts) |
||||
|
{ |
||||
|
Hosts = new List<string>(hosts).AsReadOnly(); |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<string> Hosts { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,287 @@ |
|||||
|
// 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.Net; |
||||
|
using System.Net.Http; |
||||
|
using System.Net.Sockets; |
||||
|
using System.Reactive.Subjects; |
||||
|
using System.Runtime.ExceptionServices; |
||||
|
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 class IngressService : IApplicationProcessor |
||||
|
{ |
||||
|
private List<WebApplication> _webApplications = new List<WebApplication>(); |
||||
|
private readonly ILogger _logger; |
||||
|
|
||||
|
public IngressService(ILogger logger) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public async Task StartAsync(Model.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 = service.PortMap[binding.Port.Value][i]; |
||||
|
ports.Add(port); |
||||
|
var url = $"{binding.Protocol ?? "http"}://localhost:{port}"; |
||||
|
addresses.Add(url); |
||||
|
} |
||||
|
|
||||
|
status.Ports = ports; |
||||
|
|
||||
|
service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
_logger.LogInformation("Processing ingress rule: Path:{Path}, Host:{Host}, Service:{Service}", rule.Path, rule.Host, rule.Service); |
||||
|
|
||||
|
var targetServiceDescription = target.Description; |
||||
|
|
||||
|
var uris = new List<Uri>(); |
||||
|
|
||||
|
// For each of the target service replicas, get the base URL
|
||||
|
// based on the replica port
|
||||
|
for (int i = 0; i < targetServiceDescription.Replicas; i++) |
||||
|
{ |
||||
|
foreach (var binding in targetServiceDescription.Bindings) |
||||
|
{ |
||||
|
if (binding.Port == null) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var port = target.PortMap[binding.Port.Value][i]; |
||||
|
var url = $"{binding.Protocol ?? "http"}://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 = (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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach (var app in _webApplications) |
||||
|
{ |
||||
|
await app.StartAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task StopAsync(Model.Application application) |
||||
|
{ |
||||
|
foreach (var webApp in _webApplications) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await webApp.StopAsync(); |
||||
|
} |
||||
|
catch (OperationCanceledException) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
webApp.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class ConnectionRetryHandler : DelegatingHandler |
||||
|
{ |
||||
|
private static readonly int MaxRetries = 3; |
||||
|
private static readonly TimeSpan InitialRetryDelay = TimeSpan.FromMilliseconds(1000); |
||||
|
|
||||
|
public ConnectionRetryHandler(HttpMessageHandler innerHandler) |
||||
|
: base(innerHandler) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task<HttpResponseMessage> SendAsync( |
||||
|
HttpRequestMessage request, |
||||
|
CancellationToken cancellationToken) |
||||
|
{ |
||||
|
HttpResponseMessage? response = null; |
||||
|
var delay = InitialRetryDelay; |
||||
|
Exception? exception = null; |
||||
|
|
||||
|
for (var i = 0; i < MaxRetries; i++) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
response = await base.SendAsync(request, cancellationToken); |
||||
|
} |
||||
|
catch (HttpRequestException ex) when (ex.InnerException is SocketException) |
||||
|
{ |
||||
|
if (i == MaxRetries - 1) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
exception = ex; |
||||
|
} |
||||
|
|
||||
|
if (response != null && |
||||
|
(response.IsSuccessStatusCode || response.StatusCode != HttpStatusCode.ServiceUnavailable)) |
||||
|
{ |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
await Task.Delay(delay, cancellationToken); |
||||
|
delay *= 2; |
||||
|
} |
||||
|
|
||||
|
if (exception != null) |
||||
|
{ |
||||
|
ExceptionDispatchInfo.Throw(exception); |
||||
|
} |
||||
|
|
||||
|
throw new TimeoutException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class ServiceLoggerProvider : ILoggerProvider |
||||
|
{ |
||||
|
private readonly Subject<string> _logs; |
||||
|
|
||||
|
public ServiceLoggerProvider(Subject<string> logs) |
||||
|
{ |
||||
|
_logs = logs; |
||||
|
} |
||||
|
|
||||
|
public ILogger CreateLogger(string categoryName) |
||||
|
{ |
||||
|
return new ServiceLogger(categoryName, _logs); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private class ServiceLogger : ILogger |
||||
|
{ |
||||
|
private readonly string _categoryName; |
||||
|
private readonly Subject<string> _logs; |
||||
|
|
||||
|
public ServiceLogger(string categoryName, Subject<string> logs) |
||||
|
{ |
||||
|
_categoryName = categoryName; |
||||
|
_logs = logs; |
||||
|
} |
||||
|
|
||||
|
public IDisposable? BeginScope<TState>(TState state) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public bool IsEnabled(LogLevel logLevel) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) |
||||
|
{ |
||||
|
_logs.OnNext($"[{logLevel}]: {formatter(state, exception)}"); |
||||
|
|
||||
|
if (exception != null) |
||||
|
{ |
||||
|
_logs.OnNext(exception.ToString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// 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.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Model |
||||
|
{ |
||||
|
public class IngressRule |
||||
|
{ |
||||
|
public IngressRule(string? host, string? path, string service) |
||||
|
{ |
||||
|
Host = host; |
||||
|
Path = path; |
||||
|
Service = service; |
||||
|
} |
||||
|
|
||||
|
public string? Host { get; } |
||||
|
public string? Path { get; } |
||||
|
public string Service { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// 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.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Model |
||||
|
{ |
||||
|
public class IngressRunInfo : RunInfo |
||||
|
{ |
||||
|
public IngressRunInfo(List<IngressRule> rules) |
||||
|
{ |
||||
|
Rules = rules; |
||||
|
} |
||||
|
|
||||
|
public List<IngressRule> Rules { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// 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.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting.Model |
||||
|
{ |
||||
|
public class IngressStatus : ReplicaStatus |
||||
|
{ |
||||
|
public IngressStatus(Service service, string name) : base(service, name) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,213 @@ |
|||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Net.Http; |
||||
|
using System.Net.WebSockets; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Proxy |
||||
|
{ |
||||
|
internal static class ProxyAdvancedExtensions |
||||
|
{ |
||||
|
private static readonly string[] NotForwardedWebSocketHeaders = new[] { "Connection", "Host", "Upgrade", "Sec-WebSocket-Accept", "Sec-WebSocket-Protocol", "Sec-WebSocket-Key", "Sec-WebSocket-Version", "Sec-WebSocket-Extensions" }; |
||||
|
private const int DefaultWebSocketBufferSize = 4096; |
||||
|
private const int StreamCopyBufferSize = 81920; |
||||
|
|
||||
|
public static async Task ProxyRequest(this HttpContext context, HttpMessageInvoker invoker, Uri destinationUri) |
||||
|
{ |
||||
|
if (context == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
if (destinationUri == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(destinationUri)); |
||||
|
} |
||||
|
|
||||
|
if (context.WebSockets.IsWebSocketRequest) |
||||
|
{ |
||||
|
await context.AcceptProxyWebSocketRequest(destinationUri.ToWebSocketScheme()); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
using (var requestMessage = context.CreateProxyHttpRequest(destinationUri)) |
||||
|
{ |
||||
|
using (var responseMessage = await context.SendProxyHttpRequest(invoker, requestMessage)) |
||||
|
{ |
||||
|
await context.CopyProxyHttpResponse(responseMessage); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static Uri ToWebSocketScheme(this Uri uri) |
||||
|
{ |
||||
|
if (uri == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(uri)); |
||||
|
} |
||||
|
|
||||
|
var uriBuilder = new UriBuilder(uri); |
||||
|
if (string.Equals(uriBuilder.Scheme, "https", StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
uriBuilder.Scheme = "wss"; |
||||
|
} |
||||
|
else if (string.Equals(uriBuilder.Scheme, "http", StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
uriBuilder.Scheme = "ws"; |
||||
|
} |
||||
|
|
||||
|
return uriBuilder.Uri; |
||||
|
} |
||||
|
|
||||
|
public static HttpRequestMessage CreateProxyHttpRequest(this HttpContext context, Uri uri) |
||||
|
{ |
||||
|
var request = context.Request; |
||||
|
|
||||
|
var requestMessage = new HttpRequestMessage(); |
||||
|
var requestMethod = request.Method; |
||||
|
if (!HttpMethods.IsGet(requestMethod) && |
||||
|
!HttpMethods.IsHead(requestMethod) && |
||||
|
!HttpMethods.IsDelete(requestMethod) && |
||||
|
!HttpMethods.IsTrace(requestMethod)) |
||||
|
{ |
||||
|
var streamContent = new StreamContent(request.Body); |
||||
|
requestMessage.Content = streamContent; |
||||
|
} |
||||
|
|
||||
|
// Copy the request headers
|
||||
|
foreach (var header in request.Headers) |
||||
|
{ |
||||
|
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null) |
||||
|
{ |
||||
|
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
requestMessage.Headers.Host = uri.Authority; |
||||
|
requestMessage.RequestUri = uri; |
||||
|
requestMessage.Method = new HttpMethod(request.Method); |
||||
|
|
||||
|
return requestMessage; |
||||
|
} |
||||
|
|
||||
|
public static async Task<bool> AcceptProxyWebSocketRequest(this HttpContext context, Uri destinationUri) |
||||
|
{ |
||||
|
if (context == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
if (destinationUri == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(destinationUri)); |
||||
|
} |
||||
|
if (!context.WebSockets.IsWebSocketRequest) |
||||
|
{ |
||||
|
throw new InvalidOperationException(); |
||||
|
} |
||||
|
|
||||
|
using var client = new ClientWebSocket(); |
||||
|
foreach (var protocol in context.WebSockets.WebSocketRequestedProtocols) |
||||
|
{ |
||||
|
client.Options.AddSubProtocol(protocol); |
||||
|
} |
||||
|
|
||||
|
foreach (var headerEntry in context.Request.Headers) |
||||
|
{ |
||||
|
if (!NotForwardedWebSocketHeaders.Contains(headerEntry.Key, StringComparer.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
client.Options.SetRequestHeader(headerEntry.Key, headerEntry.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await client.ConnectAsync(destinationUri, context.RequestAborted); |
||||
|
} |
||||
|
catch (WebSocketException) |
||||
|
{ |
||||
|
context.Response.StatusCode = 400; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
using var server = await context.WebSockets.AcceptWebSocketAsync(client.SubProtocol); |
||||
|
|
||||
|
var bufferSize = DefaultWebSocketBufferSize; |
||||
|
await Task.WhenAll(PumpWebSocket(client, server, bufferSize, context.RequestAborted), PumpWebSocket(server, client, bufferSize, context.RequestAborted)); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static async Task PumpWebSocket(WebSocket source, WebSocket destination, int bufferSize, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (bufferSize <= 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(bufferSize)); |
||||
|
} |
||||
|
|
||||
|
var buffer = new byte[bufferSize]; |
||||
|
while (true) |
||||
|
{ |
||||
|
WebSocketReceiveResult result; |
||||
|
try |
||||
|
{ |
||||
|
result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken); |
||||
|
} |
||||
|
catch (OperationCanceledException) |
||||
|
{ |
||||
|
await destination.CloseOutputAsync(WebSocketCloseStatus.EndpointUnavailable, null, cancellationToken); |
||||
|
return; |
||||
|
} |
||||
|
if (result.MessageType == WebSocketMessageType.Close) |
||||
|
{ |
||||
|
await destination.CloseOutputAsync(source.CloseStatus!.Value, source.CloseStatusDescription, cancellationToken); |
||||
|
return; |
||||
|
} |
||||
|
await destination.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static Task<HttpResponseMessage> SendProxyHttpRequest(this HttpContext context, HttpMessageInvoker invoker, HttpRequestMessage requestMessage) |
||||
|
{ |
||||
|
if (requestMessage == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(requestMessage)); |
||||
|
} |
||||
|
|
||||
|
return invoker.SendAsync(requestMessage, context.RequestAborted); |
||||
|
} |
||||
|
|
||||
|
public static async Task CopyProxyHttpResponse(this HttpContext context, HttpResponseMessage responseMessage) |
||||
|
{ |
||||
|
if (responseMessage == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(responseMessage)); |
||||
|
} |
||||
|
|
||||
|
var response = context.Response; |
||||
|
|
||||
|
response.StatusCode = (int)responseMessage.StatusCode; |
||||
|
foreach (var header in responseMessage.Headers) |
||||
|
{ |
||||
|
response.Headers[header.Key] = header.Value.ToArray(); |
||||
|
} |
||||
|
|
||||
|
foreach (var header in responseMessage.Content.Headers) |
||||
|
{ |
||||
|
response.Headers[header.Key] = header.Value.ToArray(); |
||||
|
} |
||||
|
|
||||
|
// SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response.
|
||||
|
response.Headers.Remove("transfer-encoding"); |
||||
|
|
||||
|
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync()) |
||||
|
{ |
||||
|
await responseStream.CopyToAsync(response.Body, StreamCopyBufferSize, context.RequestAborted); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// 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.Text; |
||||
|
|
||||
|
namespace Microsoft.Tye.ConfigModel |
||||
|
{ |
||||
|
public class ConfigIngress |
||||
|
{ |
||||
|
public string Name { get; set; } = default!; |
||||
|
public int? Replicas { get; set; } |
||||
|
public List<ConfigIngressRule> Rules { get; set; } = new List<ConfigIngressRule>(); |
||||
|
public List<ConfigIngressServiceBinding> Bindings { get; set; } = new List<ConfigIngressServiceBinding>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// 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.Text; |
||||
|
|
||||
|
namespace Microsoft.Tye.ConfigModel |
||||
|
{ |
||||
|
public class ConfigIngressRule |
||||
|
{ |
||||
|
public string? Path { get; set; } |
||||
|
public string? Host { get; set; } |
||||
|
public string? Service { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// 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.Text; |
||||
|
|
||||
|
namespace Microsoft.Tye.ConfigModel |
||||
|
{ |
||||
|
public class ConfigIngressServiceBinding |
||||
|
{ |
||||
|
public string? Name { get; set; } |
||||
|
public bool AutoAssignPort { get; set; } |
||||
|
public int? Port { get; set; } |
||||
|
public string? Protocol { get; set; } // HTTP or HTTPS
|
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue