From 1483fb7c3c1ec3f764e7123a5319cf52a788fb32 Mon Sep 17 00:00:00 2001 From: jsedlak-microsoft <65620804+jsedlak-microsoft@users.noreply.github.com> Date: Thu, 14 Mar 2024 08:20:54 -0400 Subject: [PATCH] Add yarp.ingress.kubernetes.io/route-queryparameters to allow ingress forking based on query parameters (#2436) * Add QueryParameters routing for Kubernetes.Ingress * Update documentation to be plural * retrigger checks * Fix validation file * Update docs --------- Co-authored-by: Jeff Sedlak Co-authored-by: Miha Zupan --- samples/KubernetesIngress.Sample/README.md | 32 +++++++++++ .../Converters/YarpIngressOptions.cs | 22 +++++++- .../Converters/YarpParser.cs | 12 +++-- .../IngressConversionTests.cs | 1 + .../route-queryparameters/clusters.json | 18 +++++++ .../route-queryparameters/ingress.yaml | 53 +++++++++++++++++++ .../route-queryparameters/routes.json | 29 ++++++++++ 7 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 test/Kubernetes.Tests/testassets/route-queryparameters/clusters.json create mode 100644 test/Kubernetes.Tests/testassets/route-queryparameters/ingress.yaml create mode 100644 test/Kubernetes.Tests/testassets/route-queryparameters/routes.json diff --git a/samples/KubernetesIngress.Sample/README.md b/samples/KubernetesIngress.Sample/README.md index c8f39e49c..8a26cb36f 100644 --- a/samples/KubernetesIngress.Sample/README.md +++ b/samples/KubernetesIngress.Sample/README.md @@ -57,6 +57,17 @@ metadata: - another-header-value Mode: Contains IsCaseSensitive: false + yarp.ingress.kubernetes.io/route-queryparameters: | + - Name: the-queryparameters-key + Values: + - the-queryparameters-value + Mode: Contains + IsCaseSensitive: false + - Name: another-queryparameters-key + Values: + - another-queryparameters-value + Mode: Contains + IsCaseSensitive: false spec: rules: - http: @@ -86,6 +97,7 @@ The table below lists the available annotations. |yarp.ingress.kubernetes.io/session-affinity|[SessionAffinityConfig](https://microsoft.github.io/reverse-proxy/api/Yarp.ReverseProxy.Configuration.SessionAffinityConfig.html)| |yarp.ingress.kubernetes.io/transforms|List>| |yarp.ingress.kubernetes.io/route-headers|List<[RouteHeader](https://microsoft.github.io/reverse-proxy/api/Yarp.ReverseProxy.Configuration.RouteHeader.html)>| +|yarp.ingress.kubernetes.io/route-queryparameters|List<[RouteQueryParameter](https://microsoft.github.io/reverse-proxy/api/Yarp.ReverseProxy.Configuration.RouteQueryParameter.html)>| |yarp.ingress.kubernetes.io/route-order|int| #### Authorization Policy @@ -213,6 +225,26 @@ yarp.ingress.kubernetes.io/route-headers: | IsCaseSensitive: false ``` +#### Route QueryParameters + +`route-queryparameters` are the YAML representation of YARP [Parameter Based Routing](https://microsoft.github.io/reverse-proxy/articles/queryparameter-routing.html). + +See https://microsoft.github.io/reverse-proxy/api/Yarp.ReverseProxy.Configuration.RouteQueryParameter.html. + +``` +yarp.ingress.kubernetes.io/route-queryparameters: | + - Name: the-queryparameter-name + Values: + - the-queryparameter-value + Mode: Contains + IsCaseSensitive: false + - Name: another-queryparameter-name + Values: + - another-queryparameter-value + Mode: Contains + IsCaseSensitive: false +``` + #### Route Order See https://microsoft.github.io/reverse-proxy/api/Yarp.ReverseProxy.Configuration.RouteConfig.html#Yarp_ReverseProxy_Configuration_RouteConfig_Order. diff --git a/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs b/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs index c648405d5..aa8184732 100644 --- a/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs +++ b/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs @@ -25,10 +25,11 @@ internal sealed class YarpIngressOptions public HealthCheckConfig HealthCheck { get; set; } public Dictionary RouteMetadata { get; set; } public List RouteHeaders { get; set; } + public List RouteQueryParameters { get; set; } public int? RouteOrder { get; set; } } -internal sealed class RouteHeaderWapper +internal sealed class RouteHeaderWrapper { public string Name { get; init; } public List Values { get; init; } @@ -46,3 +47,22 @@ public RouteHeader ToRouteHeader() }; } } + +internal sealed class RouteQueryParameterWrapper +{ + public string Name { get; init; } + public List Values { get; init; } + public QueryParameterMatchMode Mode { get; init; } + public bool IsCaseSensitive { get; init; } + + public RouteQueryParameter ToRouteQueryParameter() + { + return new RouteQueryParameter + { + Name = Name, + Values = Values, + Mode = Mode, + IsCaseSensitive = IsCaseSensitive + }; + } +} diff --git a/src/Kubernetes.Controller/Converters/YarpParser.cs b/src/Kubernetes.Controller/Converters/YarpParser.cs index 529cf7359..57fb803c3 100644 --- a/src/Kubernetes.Controller/Converters/YarpParser.cs +++ b/src/Kubernetes.Controller/Converters/YarpParser.cs @@ -134,7 +134,8 @@ private static RouteConfig CreateRoute(YarpIngressContext ingressContext, V1HTTP { Hosts = host is not null ? new[] { host } : Array.Empty(), Path = pathMatch, - Headers = ingressContext.Options.RouteHeaders + Headers = ingressContext.Options.RouteHeaders, + QueryParameters = ingressContext.Options.RouteQueryParameters }, ClusterId = cluster.ClusterId, RouteId = $"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}", @@ -276,8 +277,13 @@ private static YarpIngressOptions HandleAnnotations(YarpIngressContext context, } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-headers", out var routeHeaders)) { - // YamlDeserializer does not support IReadOnlyList in RouteHeader for now, so we use RouteHeaderWapper to solve this problem. - options.RouteHeaders = YamlDeserializer.Deserialize>(routeHeaders).Select(p => p.ToRouteHeader()).ToList(); + // YamlDeserializer does not support IReadOnlyList in RouteHeader for now, so we use RouteHeaderWrapper to solve this problem. + options.RouteHeaders = YamlDeserializer.Deserialize>(routeHeaders).Select(p => p.ToRouteHeader()).ToList(); + } + if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-queryparameters", out var routeQueryParameters)) + { + // YamlDeserializer does not support IReadOnlyList in RouteParameters for now, so we use RouterQueryParameterWrapper to solve this problem. + options.RouteQueryParameters = YamlDeserializer.Deserialize>(routeQueryParameters).Select(p => p.ToRouteQueryParameter()).ToList(); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-order", out var routeOrder)) { diff --git a/test/Kubernetes.Tests/IngressConversionTests.cs b/test/Kubernetes.Tests/IngressConversionTests.cs index beab139f2..0eedfd522 100644 --- a/test/Kubernetes.Tests/IngressConversionTests.cs +++ b/test/Kubernetes.Tests/IngressConversionTests.cs @@ -51,6 +51,7 @@ public IngressConversionTests() [InlineData("multiple-ingresses-one-svc")] [InlineData("multiple-namespaces")] [InlineData("route-metadata")] + [InlineData("route-queryparameters")] [InlineData("route-headers")] [InlineData("route-order")] [InlineData("missing-svc")] diff --git a/test/Kubernetes.Tests/testassets/route-queryparameters/clusters.json b/test/Kubernetes.Tests/testassets/route-queryparameters/clusters.json new file mode 100644 index 000000000..4ad10140a --- /dev/null +++ b/test/Kubernetes.Tests/testassets/route-queryparameters/clusters.json @@ -0,0 +1,18 @@ +[ + { + "ClusterId": "frontend.default:80", + "LoadBalancingPolicy": null, + "SessionAffinity": null, + "HealthCheck": null, + "HttpClient": null, + "HttpRequest": null, + "Destinations": { + "http://10.244.2.38:80": { + "Address": "http://10.244.2.38:80", + "Health": null, + "Metadata": null + } + }, + "Metadata": null + } +] diff --git a/test/Kubernetes.Tests/testassets/route-queryparameters/ingress.yaml b/test/Kubernetes.Tests/testassets/route-queryparameters/ingress.yaml new file mode 100644 index 000000000..34ab549dc --- /dev/null +++ b/test/Kubernetes.Tests/testassets/route-queryparameters/ingress.yaml @@ -0,0 +1,53 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: minimal-ingress + namespace: default + annotations: + yarp.ingress.kubernetes.io/route-metadata: | + foo: bar + another-key: another-value + yarp.ingress.kubernetes.io/route-queryparameters: | + - Name: the-queryparameter-key + Values: + - the-queryparameter-value + Mode: Contains + IsCaseSensitive: false +spec: + rules: + - http: + paths: + - path: /foo + pathType: Prefix + backend: + service: + name: frontend + port: + number: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: default +spec: + selector: + app: frontend + ports: + - name: https + port: 80 + targetPort: 80 + type: ClusterIP +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: frontend + namespace: default +subsets: + - addresses: + - ip: 10.244.2.38 + ports: + - name: https + port: 80 + protocol: TCP diff --git a/test/Kubernetes.Tests/testassets/route-queryparameters/routes.json b/test/Kubernetes.Tests/testassets/route-queryparameters/routes.json new file mode 100644 index 000000000..70a48af81 --- /dev/null +++ b/test/Kubernetes.Tests/testassets/route-queryparameters/routes.json @@ -0,0 +1,29 @@ +[ + { + "RouteId": "minimal-ingress.default:/foo", + "Match": { + "Methods": null, + "Hosts": [], + "Path": "/foo/{**catch-all}", + "Headers": null, + "QueryParameters": [ + { + "Name": "the-queryparameter-key", + "Values": [ "the-queryparameter-value" ], + "Mode": "Contains", + "IsCaseSensitive": false + } + ] + }, + "Order": null, + "ClusterId": "frontend.default:80", + "AuthorizationPolicy": null, + "RateLimiterPolicy": null, + "CorsPolicy": null, + "Metadata": { + "foo": "bar", + "another-key": "another-value" + }, + "Transforms": null + } +]