Skip to content

Commit

Permalink
Add support for external names to Kubernetes Controller (#2231)
Browse files Browse the repository at this point in the history
* Add support for external names to Kubernetes Controller

* Code Review Refactor of shared methods

* Update src/Kubernetes.Controller/Converters/YarpParser.cs

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>

* Code Review

---------

Co-authored-by: Karl Martin-Chambers <karl.martin-chambers@aveva.com>
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
  • Loading branch information
3 people committed Sep 4, 2023
1 parent 739c24b commit 47f1e7c
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 43 deletions.
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
19 changes: 19 additions & 0 deletions test/Kubernetes.Tests/testassets/external-name-ingress/routes.json
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
}
]

0 comments on commit 47f1e7c

Please sign in to comment.