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 Route Select for Rate Limit #316

Merged
merged 3 commits into from
Jan 12, 2024
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
73 changes: 72 additions & 1 deletion testsuite/gateway/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Classes related to Gateways"""
import enum
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Optional, TYPE_CHECKING
from typing import Any, Optional, TYPE_CHECKING, Literal, List
jsmolar marked this conversation as resolved.
Show resolved Hide resolved

from httpx import Client

Expand Down Expand Up @@ -45,6 +46,68 @@ def reference(self) -> dict[str, Any]:
port: Optional[int] = None


class HTTPMethod(enum.Enum):
"""HTTP methods supported by Matchers"""

CONNECT = "CONNECT"
DELETE = "DELETE"
GET = "GET"
HEAD = "HEAD"
OPTIONS = "OPTIONS"
PATCH = "PATCH"
POST = "POST"
PUT = "PUT"
TRACE = "TRACE"


class MatchType(enum.Enum):
"""MatchType specifies the semantics of how HTTP header values should be compared."""

EXACT = "Exact"
PATH_PREFIX = "PathPrefix"
REGULAR_EXPRESSION = "RegularExpression"


@dataclass
class PathMatch:
"""HTTPPathMatch describes how to select an HTTP route by matching the HTTP request path."""

type: Optional[MatchType] = None
value: Optional[str] = None


@dataclass
class HeadersMatch:
"""HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request headers."""

name: str
value: str
type: Optional[Literal[MatchType.EXACT, MatchType.REGULAR_EXPRESSION]] = None


@dataclass
class QueryParamsMatch:
"""HTTPQueryParamMatch describes how to select a HTTP route by matching HTTP query parameters."""

name: str
value: str
type: Optional[Literal[MatchType.EXACT, MatchType.REGULAR_EXPRESSION]] = None


@dataclass
class RouteMatch:
"""
HTTPRouteMatch defines the predicate used to match requests to a given action.
Multiple match types are ANDed together, i.e. the match will evaluate to true only if all conditions are satisfied.
https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteMatch
"""

path: Optional[PathMatch] = None
headers: Optional[List[HeadersMatch]] = None
query_params: Optional[List[QueryParamsMatch]] = None
method: Optional[HTTPMethod] = None


class Gateway(LifecycleObject, Referencable):
"""
Abstraction layer for a Gateway sitting between end-user and Kuadrant
Expand Down Expand Up @@ -99,6 +162,14 @@ def remove_hostname(self, hostname: str):
def remove_all_hostnames(self):
"""Remove all hostnames from the Route"""

@abstractmethod
def add_rule(self, backend: "Httpbin", *route_matches: RouteMatch):
"""Adds rule to the Route"""

@abstractmethod
def remove_all_rules(self):
"""Remove all rules from the Route"""

@abstractmethod
def add_backend(self, backend: "Httpbin", prefix):
"""Adds another backend to the Route, with specific prefix"""
Expand Down
28 changes: 16 additions & 12 deletions testsuite/gateway/gateway_api/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from httpx import Client

from testsuite.httpx import KuadrantClient
from testsuite.gateway import Gateway, GatewayRoute
from testsuite.gateway import Gateway, GatewayRoute, PathMatch, MatchType, RouteMatch
from testsuite.openshift.client import OpenShiftClient
from testsuite.openshift import OpenShiftObject, modify
from testsuite.utils import asdict

if typing.TYPE_CHECKING:
from testsuite.openshift.httpbin import Httpbin
Expand Down Expand Up @@ -73,17 +74,20 @@ def remove_all_hostnames(self):
self.model.spec.hostnames = []

@modify
def set_match(self, backend: "Httpbin", path_prefix: str = None):
"""Limits HTTPRoute to a certain path"""
match = {}
if path_prefix:
match["path"] = {"value": path_prefix, "type": "PathPrefix"}
for rule in self.model.spec.rules:
for ref in rule.backendRefs:
if backend.reference["name"] == ref["name"]:
rule["matches"] = [match]
return
raise NameError("This backend is not assigned to this Route")
def add_rule(self, backend: "Httpbin", *route_matches: RouteMatch):
"""Adds rule to the Route"""
rules = {"backendRefs": [backend.reference]}
matches = list(route_matches)
if len(matches) == 0:
matches.append(RouteMatch(path=PathMatch(type=MatchType.PATH_PREFIX, value="/")))

rules["matches"] = [asdict(match) for match in matches]
self.model.spec.rules.append(rules)

@modify
def remove_all_rules(self):
"""Remove all rules from the Route"""
self.model.spec.rules = []

@modify
def add_backend(self, backend: "Httpbin", prefix="/"):
Expand Down
34 changes: 30 additions & 4 deletions testsuite/policy/rate_limit_policy.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""RateLimitPolicy related objects"""
from dataclasses import dataclass
from time import sleep
from typing import Iterable, Literal
from typing import Iterable, Literal, Optional, List

import openshift as oc

from testsuite.policy.authorization import Pattern
from testsuite.policy.authorization import Rule
from testsuite.utils import asdict
from testsuite.gateway import Referencable
from testsuite.gateway import Referencable, RouteMatch
from testsuite.openshift.client import OpenShiftClient
from testsuite.openshift import OpenShiftObject, modify

Expand All @@ -21,6 +21,23 @@ class Limit:
unit: Literal["second", "minute", "day"] = "second"


@dataclass
class RouteSelector:
"""
RouteSelector is an object composed of a set of HTTPRouteMatch objects (from Gateway API -
HTTPPathMatch, HTTPHeaderMatch, HTTPQueryParamMatch, HTTPMethodMatch),
and an additional hostnames field.
https://docs.kuadrant.io/kuadrant-operator/doc/reference/route-selectors/#routeselector
"""

matches: Optional[list[RouteMatch]] = None
hostnames: Optional[list[str]] = None

def __init__(self, *matches: RouteMatch, hostnames: Optional[List[str]] = None):
self.matches = list(matches) if matches else []
self.hostnames = hostnames


class RateLimitPolicy(OpenShiftObject):
"""RateLimitPolicy (or RLP for short) object, used for applying rate limiting rules to a Gateway/HTTPRoute"""

Expand All @@ -40,7 +57,14 @@ def create_instance(cls, openshift: OpenShiftClient, name, target: Referencable,
return cls(model, context=openshift.context)

@modify
def add_limit(self, name, limits: Iterable[Limit], when: Iterable[Pattern] = None, counters: list[str] = None):
def add_limit(
self,
name,
limits: Iterable[Limit],
when: Iterable[Rule] = None,
counters: list[str] = None,
route_selectors: Iterable[RouteSelector] = None,
):
"""Add another limit"""
limit: dict = {
"rates": [asdict(limit) for limit in limits],
Expand All @@ -49,6 +73,8 @@ def add_limit(self, name, limits: Iterable[Limit], when: Iterable[Pattern] = Non
limit["when"] = [asdict(rule) for rule in when]
if counters:
limit["counters"] = counters
if route_selectors:
limit["routeSelectors"] = [asdict(rule) for rule in route_selectors]
self.model.spec.limits[name] = limit

def wait_for_ready(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests that HTTPRoute spec.routes.matches changes are reconciled when changed."""
from testsuite.gateway import RouteMatch, PathMatch


def test_matches(client, backend, route, resilient_request):
Expand All @@ -12,7 +13,8 @@ def test_matches(client, backend, route, resilient_request):
response = client.get("/get")
assert response.status_code == 200

route.set_match(backend, path_prefix="/anything")
route.remove_all_rules()
route.add_rule(backend, RouteMatch(path=PathMatch(value="/anything")))

response = resilient_request("/get", expected_status=404)
assert response.status_code == 404, "Matches were not reconciled"
Expand Down
2 changes: 2 additions & 0 deletions testsuite/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ def _asdict_recurse(obj):
result[field.name] = type(value)(_asdict_recurse(i) for i in value)
elif isinstance(value, dict):
result[field.name] = type(value)((_asdict_recurse(k), _asdict_recurse(v)) for k, v in value.items())
elif isinstance(value, enum.Enum):
result[field.name] = value.value
else:
result[field.name] = deepcopy(value)
return result