diff --git a/testsuite/gateway/__init__.py b/testsuite/gateway/__init__.py index b8d26068..4412188b 100644 --- a/testsuite/gateway/__init__.py +++ b/testsuite/gateway/__init__.py @@ -1,13 +1,15 @@ """Classes related to Gateways""" +import abc +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 from httpx import Client from testsuite.certificates import Certificate from testsuite.lifecycle import LifecycleObject -from testsuite.utils import asdict +from testsuite.utils import asdict, _asdict_recurse if TYPE_CHECKING: from testsuite.openshift.client import OpenShiftClient @@ -45,6 +47,94 @@ 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 + + # def asdict(self): + # """Custom dict due to nested structure of matchers.""" + # return {"path": _asdict_recurse(self, False)} + + +@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 + + # def asdict(self): + # """Custom dict due to nested structure of matchers.""" + # return {"headers": [_asdict_recurse(self, False)]} + + +@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 + + # def asdict(self): + # """Custom dict due to nested structure of matchers.""" + # return {"queryParams": [_asdict_recurse(self, False)]} + + +@dataclass +class MethodMatch: + """ + HTTPMethod describes how to select a HTTP route by matching the HTTP method. The value is expected in upper case. + """ + + value: HTTPMethod = None + + def asdict(self): + """Custom dict due to nested structure of matchers.""" + return {"method": self.value.value} + + +@dataclass +class RouteMatch: + """ + Abstract Dataclass for HTTPRouteMatch. + API specification consists of two layers: HTTPRouteMatch which can contain 4 matchers (see offsprings). + We merged this to a single Matcher representation for simplicity, which is why we need custom `asdict` methods. + 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: HTTPMethod = None + + class Gateway(LifecycleObject, Referencable): """ Abstraction layer for a Gateway sitting between end-user and Kuadrant diff --git a/testsuite/policy/rate_limit_policy.py b/testsuite/policy/rate_limit_policy.py index 069442ed..9343fa0a 100644 --- a/testsuite/policy/rate_limit_policy.py +++ b/testsuite/policy/rate_limit_policy.py @@ -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 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, HTTPMatcher from testsuite.openshift.client import OpenShiftClient from testsuite.openshift import OpenShiftObject, modify @@ -21,6 +21,17 @@ class Limit: unit: Literal["second", "minute", "day"] = "second" +@dataclass +class RouteSelect: + """ + HTRTPPathMatch, HTTPHeaderMatch, HTTPQueryParamMatch, HTTPMethodMatch + https://docs.kuadrant.io/kuadrant-operator/doc/reference/route-selectors/#routeselector + """ + + matches: Optional[list[HTTPMatcher]] = None + hostnames: Optional[list[str]] = None + + class RateLimitPolicy(OpenShiftObject): """RateLimitPolicy (or RLP for short) object, used for applying rate limiting rules to a Gateway/HTTPRoute""" @@ -40,7 +51,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[RouteSelect] = None, + ): """Add another limit""" limit: dict = { "rates": [asdict(limit) for limit in limits], @@ -49,6 +67,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): diff --git a/testsuite/utils.py b/testsuite/utils.py index 2697a727..e9116fa6 100644 --- a/testsuite/utils.py +++ b/testsuite/utils.py @@ -138,9 +138,6 @@ def asdict(obj) -> dict[str, JSONValues]: def _asdict_recurse(obj): - if hasattr(obj, "asdict"): - return obj.asdict() - if not is_dataclass(obj): return deepcopy(obj) @@ -156,6 +153,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