Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for external names to Kubernetes Controller #2231

Merged
merged 4 commits into from
Sep 4, 2023
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)
MihaZupan marked this conversation as resolved.
Show resolved Hide resolved
{
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)
MihaZupan marked this conversation as resolved.
Show resolved Hide resolved
{
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
}
]
Loading