Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 84 additions & 43 deletions src/Kubernetes.Controller/Converters/YarpParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Yarp.Kubernetes.Controller.Converters;

internal static class YarpParser
{
private const string ExternalNameServiceType = "ExternalName";
private static readonly Deserializer YamlDeserializer = new();

internal static void ConvertFromKubernetesIngress(YarpIngressContext ingressContext, YarpConfigContext configContext)
Expand Down Expand Up @@ -42,42 +43,50 @@ private static void HandleIngressRule(YarpIngressContext ingressContext, List<En
var service = ingressContext.Services.SingleOrDefault(s => s.Metadata.Name == path.Backend.Service.Name);
if (service.Spec != null)
{
var servicePort = service.Spec.Ports.SingleOrDefault(p => MatchesPort(p, path.Backend.Service.Port));
if (servicePort != null)
if (string.Equals(service.Spec.Type, ExternalNameServiceType, StringComparison.OrdinalIgnoreCase))
{
HandleIngressRulePath(ingressContext, servicePort, endpoints, defaultSubsets, rule, path, configContext);
HandleExternalIngressRulePath(ingressContext, service.Spec.ExternalName, rule, path, configContext);
}
else
{
var servicePort = service.Spec.Ports.SingleOrDefault(p => MatchesPort(p, path.Backend.Service.Port));
if (servicePort != null)
{
HandleIngressRulePath(ingressContext, servicePort, endpoints, defaultSubsets, rule, path, configContext);
}
}
}
}
}

private static void HandleExternalIngressRulePath(YarpIngressContext ingressContext, string externalName, V1IngressRule rule, V1HTTPIngressPath path, YarpConfigContext configContext)
{
var backend = path.Backend;
var ingressServiceBackend = backend.Service;
var routes = configContext.Routes;

var cluster = GetOrAddCluster(ingressContext, configContext, ingressServiceBackend);

var pathMatch = FixupPathMatch(path);
var host = rule.Host;

routes.Add(CreateRoute(ingressContext, path, cluster, pathMatch, host));
AddDestination(cluster, ingressContext, externalName, ingressServiceBackend.Port.Number);
}

private static void HandleIngressRulePath(YarpIngressContext ingressContext, V1ServicePort servicePort, List<Endpoints> endpoints, IList<V1EndpointSubset> defaultSubsets, V1IngressRule rule, V1HTTPIngressPath path, YarpConfigContext configContext)
{
var backend = path.Backend;
var ingressServiceBackend = backend.Service;
var subsets = defaultSubsets;

var clusters = configContext.ClusterTransfers;
var routes = configContext.Routes;

if (!string.IsNullOrEmpty(ingressServiceBackend?.Name))
{
subsets = endpoints.SingleOrDefault(x => x.Name == ingressServiceBackend?.Name).Subsets;
}

// Each ingress rule path can only be for one service
var key = UpstreamName(ingressContext.Ingress.Metadata.NamespaceProperty, ingressServiceBackend);
if (!clusters.ContainsKey(key))
{
clusters.Add(key, new ClusterTransfer());
}

var cluster = clusters[key];
cluster.ClusterId = key;
cluster.LoadBalancingPolicy = ingressContext.Options.LoadBalancingPolicy;
cluster.SessionAffinity = ingressContext.Options.SessionAffinity;
cluster.HealthCheck = ingressContext.Options.HealthCheck;
cluster.HttpClientConfig = ingressContext.Options.HttpClientConfig;
var cluster = GetOrAddCluster(ingressContext, configContext, ingressServiceBackend);

// make sure cluster is present
foreach (var subset in subsets ?? Enumerable.Empty<V1EndpointSubset>())
Expand All @@ -92,40 +101,72 @@ private static void HandleIngressRulePath(YarpIngressContext ingressContext, V1S
var pathMatch = FixupPathMatch(path);
var host = rule.Host;

routes.Add(new RouteConfig()
{
Match = new RouteMatch()
{
Hosts = host is not null ? new[] { host } : Array.Empty<string>(),
Path = pathMatch,
Headers = ingressContext.Options.RouteHeaders
},
ClusterId = cluster.ClusterId,
RouteId = $"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}",
Transforms = ingressContext.Options.Transforms,
AuthorizationPolicy = ingressContext.Options.AuthorizationPolicy,
#if NET7_0_OR_GREATER
RateLimiterPolicy = ingressContext.Options.RateLimiterPolicy,
#endif
CorsPolicy = ingressContext.Options.CorsPolicy,
Metadata = ingressContext.Options.RouteMetadata,
Order = ingressContext.Options.RouteOrder,
});
routes.Add(CreateRoute(ingressContext, path, cluster, pathMatch, host));

// Add destination for every endpoint address
foreach (var address in subset.Addresses ?? Enumerable.Empty<V1EndpointAddress>())
{
var protocol = ingressContext.Options.Https ? "https" : "http";
var uri = $"{protocol}://{address.Ip}:{port.Port}";
cluster.Destinations[uri] = new DestinationConfig()
{
Address = uri
};
AddDestination(cluster, ingressContext, address.Ip, port.Port);
}
}
}
}

private static void AddDestination(ClusterTransfer cluster, YarpIngressContext ingressContext, string host, int? port)
{
var protocol = ingressContext.Options.Https ? "https" : "http";
var uri = $"{protocol}://{host}";
if (port.HasValue)
{
uri += $":{port}";
}
cluster.Destinations[uri] = new DestinationConfig()
{
Address = uri
};
}

private static RouteConfig CreateRoute(YarpIngressContext ingressContext, V1HTTPIngressPath path, ClusterTransfer cluster, string pathMatch, string host)
{
return new RouteConfig()
{
Match = new RouteMatch()
{
Hosts = host is not null ? new[] { host } : Array.Empty<string>(),
Path = pathMatch,
Headers = ingressContext.Options.RouteHeaders
},
ClusterId = cluster.ClusterId,
RouteId = $"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}",
Transforms = ingressContext.Options.Transforms,
AuthorizationPolicy = ingressContext.Options.AuthorizationPolicy,
#if NET7_0_OR_GREATER
RateLimiterPolicy = ingressContext.Options.RateLimiterPolicy,
#endif
CorsPolicy = ingressContext.Options.CorsPolicy,
Metadata = ingressContext.Options.RouteMetadata,
Order = ingressContext.Options.RouteOrder,
};
}

private static ClusterTransfer GetOrAddCluster(YarpIngressContext ingressContext, YarpConfigContext configContext, V1IngressServiceBackend ingressServiceBackend)
{
var clusters = configContext.ClusterTransfers;
// Each ingress rule path can only be for one service
var key = UpstreamName(ingressContext.Ingress.Metadata.NamespaceProperty, ingressServiceBackend);
if (!clusters.ContainsKey(key))
{
clusters.Add(key, new ClusterTransfer());
}
var cluster = clusters[key];
cluster.ClusterId = key;
cluster.LoadBalancingPolicy = ingressContext.Options.LoadBalancingPolicy;
cluster.SessionAffinity = ingressContext.Options.SessionAffinity;
cluster.HealthCheck = ingressContext.Options.HealthCheck;
cluster.HttpClientConfig = ingressContext.Options.HttpClientConfig;
return cluster;
}

private static string UpstreamName(string namespaceName, V1IngressServiceBackend ingressServiceBackend)
{
if (ingressServiceBackend is not null)
Expand Down
1 change: 1 addition & 0 deletions test/Kubernetes.Tests/IngressConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public IngressConversionTests()
[InlineData("route-order")]
[InlineData("missing-svc")]
[InlineData("port-diff-name")]
[InlineData("external-name-ingress")]
public async Task ParsingTests(string name)
{
var ingressClass = KubeResourceGenerator.CreateIngressClass("yarp", "microsoft.com/ingress-yarp", true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"ClusterId": "external-service.default:443",
"LoadBalancingPolicy": null,
"SessionAffinity": null,
"HealthCheck": null,
"HttpClient": null,
"HttpRequest": null,
"Destinations": {
"http://external-service.example.com:443": {
"Address": "http://external-service.example.com:443",
"Health": null,
"Metadata": null
}
},
"Metadata": null
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: v1
kind: Service
metadata:
name: external-service
namespace: default
spec:
type: ExternalName
externalName: external-service.example.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: external-ingress
namespace: default
spec:
rules:
- http:
paths:
- path: /foo
pathType: Prefix
backend:
service:
name: external-service
port:
number: 443
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"RouteId": "external-ingress.default:/foo",
"Match": {
"Methods": null,
"Hosts": [],
"Path": "/foo/{**catch-all}",
"Headers": null,
"QueryParameters": null
},
"Order": null,
"ClusterId": "external-service.default:443",
"AuthorizationPolicy": null,
"RateLimiterPolicy": null,
"CorsPolicy": null,
"Metadata": null,
"Transforms": null
}
]