diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be86d30e3..f9c2c08a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,15 @@ gateway, route, authorization) - `multicluster/` for multi-cluster scenarios - Group tests by feature area (authorino, limitador, gateway, etc.) +**Using Constants:** + +- All shared magic numbers (timeouts, ports, retry configs) live in `testsuite/utils/constants.py` +- Import constants instead of hardcoding values, e.g. `from testsuite.utils.constants import K8S_WAIT_UNTIL_TIMEOUT` +- When adding a new timeout, port, or retry value, check `constants.py` first. A suitable constant may already exist +- If no suitable constant exists, add a new one to `constants.py` rather than hardcoding the value locally +- Name constants clearly by domain and purpose (e.g. `PROMETHEUS_VERIFY_NO_TARGETS_RETRIES`) +- Follow the `*_MAX_RETRIES` pattern for retry counts and `*_TIMEOUT` for durations + ### Reformat, Commit Acceptance & Cleanup Before committing your changes, make sure the code is properly formatted, and all commit checks pass. Otherwise, your pull request may fail the GitHub Actions **Code Static Analysis** checks. diff --git a/testsuite/backend/__init__.py b/testsuite/backend/__init__.py index fa45f0ffd..f0af91507 100644 --- a/testsuite/backend/__init__.py +++ b/testsuite/backend/__init__.py @@ -6,6 +6,7 @@ from testsuite.gateway import Referencable from testsuite.lifecycle import LifecycleObject from testsuite.kubernetes.client import KubernetesClient +from testsuite.utils.constants import HTTP_API_PORT class Backend(LifecycleObject, Referencable): @@ -21,7 +22,13 @@ def __init__(self, cluster: KubernetesClient, name: str, label: str): @property def reference(self): - return {"group": "", "kind": "Service", "port": 8080, "name": self.name, "namespace": self.cluster.project} + return { + "group": "", + "kind": "Service", + "port": HTTP_API_PORT, + "name": self.name, + "namespace": self.cluster.project, + } @property def url(self): diff --git a/testsuite/backend/httpbin.py b/testsuite/backend/httpbin.py index 255b41b49..078a6bde0 100644 --- a/testsuite/backend/httpbin.py +++ b/testsuite/backend/httpbin.py @@ -5,6 +5,7 @@ from testsuite.kubernetes.client import KubernetesClient from testsuite.kubernetes.deployment import Deployment from testsuite.kubernetes.service import Service, ServicePort +from testsuite.utils.constants import HTTP_API_PORT class Httpbin(Backend): @@ -22,7 +23,7 @@ def commit(self): self.name, container_name="httpbin", image=self.image, - ports={"api": 8080}, + ports={"api": HTTP_API_PORT}, selector=Selector(matchLabels=match_labels), labels={"app": self.label}, ) @@ -33,7 +34,7 @@ def commit(self): self.cluster, self.name, selector=match_labels, - ports=[ServicePort(name="http", port=8080, targetPort="api")], + ports=[ServicePort(name="http", port=HTTP_API_PORT, targetPort="api")], labels={"app": self.label}, ) self.service.commit() diff --git a/testsuite/backend/llm_sim.py b/testsuite/backend/llm_sim.py index 2370fd56b..e1dd43b42 100644 --- a/testsuite/backend/llm_sim.py +++ b/testsuite/backend/llm_sim.py @@ -5,6 +5,7 @@ from testsuite.kubernetes.client import KubernetesClient from testsuite.kubernetes.deployment import Deployment from testsuite.kubernetes.service import Service, ServicePort +from testsuite.utils.constants import HTTP_API_PORT class LlmSim(Backend): @@ -23,10 +24,10 @@ def commit(self): self.name, container_name="llm-sim", image=self.image, - ports={"api": 8080}, + ports={"api": HTTP_API_PORT}, selector=Selector(matchLabels=match_labels), labels={"app": self.label}, - command_args=["--model", self.model, "--port", "8080"], + command_args=["--model", self.model, "--port", str(HTTP_API_PORT)], ) self.deployment.commit() self.deployment.wait_for_ready() @@ -35,6 +36,6 @@ def commit(self): self.cluster, self.name, selector=match_labels, - ports=[ServicePort(name="http", port=8080, targetPort="api")], + ports=[ServicePort(name="http", port=HTTP_API_PORT, targetPort="api")], ) self.service.commit() diff --git a/testsuite/backend/mockserver.py b/testsuite/backend/mockserver.py index aaab9d878..a4d8d3e63 100644 --- a/testsuite/backend/mockserver.py +++ b/testsuite/backend/mockserver.py @@ -6,6 +6,7 @@ from testsuite.kubernetes.config_map import ConfigMap from testsuite.kubernetes.deployment import Deployment, ContainerResources, ConfigMapVolume, VolumeMount from testsuite.kubernetes.service import Service, ServicePort +from testsuite.utils.constants import MOCKSERVER_INTERNAL_PORT, HTTP_API_PORT, SERVICE_READY_TIMEOUT INIT_JSON_MOUNT = "/config/mockserver" @@ -74,7 +75,7 @@ def commit(self): self.name, container_name="mockserver", image=settings["mockserver"]["image"], - ports={"api": 1080}, + ports={"api": MOCKSERVER_INTERNAL_PORT}, selector=Selector(matchLabels=match_labels), labels={"app": self.label}, resources=ContainerResources(limits_memory="2G"), @@ -89,7 +90,7 @@ def commit(self): self.cluster, self.name, selector=match_labels, - ports=[ServicePort(name="http", port=8080, targetPort="api")], + ports=[ServicePort(name="http", port=HTTP_API_PORT, targetPort="api")], labels={"app": self.label}, service_type=self.service_type, ) @@ -103,7 +104,7 @@ def delete(self): finally: super().delete() - def wait_for_ready(self, timeout=60 * 5): + def wait_for_ready(self, timeout=SERVICE_READY_TIMEOUT): """Waits until Deployment and Service is marked as ready""" self.deployment.wait_for_ready(timeout) self.service.wait_for_ready(timeout, settings["control_plane"]["slow_loadbalancers"]) diff --git a/testsuite/config/__init__.py b/testsuite/config/__init__.py index d7b758e66..69bcb108c 100644 --- a/testsuite/config/__init__.py +++ b/testsuite/config/__init__.py @@ -4,6 +4,13 @@ from testsuite.utils import hostname_to_ip from testsuite.config.tools import fetch_route, fetch_service, fetch_secret, fetch_service_ip, fetch_prometheus_url +from testsuite.utils.constants import ( + OTEL_COLLECTOR_PORT, + HTTP_API_PORT, + MOCKSERVER_INTERNAL_PORT, + VAULT_PORT, + REDIS_PORT, +) # pylint: disable=too-few-public-methods @@ -41,7 +48,7 @@ def __init__(self, name, default, **kwargs) -> None: ), DefaultValueValidator("tracing.backend", default="jaeger", is_in=["jaeger", "tempo"]), DefaultValueValidator( - "tracing.collector_url", default=fetch_service("jaeger-collector", protocol="rpc", port=4317) + "tracing.collector_url", default=fetch_service("jaeger-collector", protocol="rpc", port=OTEL_COLLECTOR_PORT) ), DefaultValueValidator("tracing.query_url", default=fetch_service_ip("jaeger-query", protocol="http", port=80)), Validator( @@ -68,20 +75,25 @@ def __init__(self, name, default, **kwargs) -> None: & Validator("dns.dns_server2.geo_code", must_exist=True, ne=None) ), Validator("dns.default_geo_server", must_exist=True, ne=None, cast=hostname_to_ip), - DefaultValueValidator("keycloak.url", default=fetch_service_ip("keycloak", protocol="http", port=8080)), + DefaultValueValidator( + "keycloak.url", default=fetch_service_ip("keycloak", protocol="http", port=HTTP_API_PORT) + ), DefaultValueValidator("keycloak.password", default=fetch_secret("credential-sso", "ADMIN_PASSWORD")), - DefaultValueValidator("mockserver.url", default=fetch_service_ip("mockserver", protocol="http", port=1080)), DefaultValueValidator( - "vault.url", default=fetch_service_ip("vault", protocol="http", port=8200, namespace="tools-vault") + "mockserver.url", default=fetch_service_ip("mockserver", protocol="http", port=MOCKSERVER_INTERNAL_PORT) + ), + DefaultValueValidator( + "vault.url", default=fetch_service_ip("vault", protocol="http", port=VAULT_PORT, namespace="tools-vault") ), DefaultValueValidator("vault.token", default="root"), - DefaultValueValidator("redis.url", default=fetch_service_ip("redis", protocol="redis", port=6379)), - DefaultValueValidator("dragonfly.url", default=fetch_service_ip("dragonfly", protocol="redis", port=6379)), - DefaultValueValidator("valkey.url", default=fetch_service_ip("valkey", protocol="redis", port=6379)), - DefaultValueValidator("mockserver.url", default=fetch_service_ip("mockserver", protocol="http", port=1080)), + DefaultValueValidator("redis.url", default=fetch_service_ip("redis", protocol="redis", port=REDIS_PORT)), + DefaultValueValidator( + "dragonfly.url", default=fetch_service_ip("dragonfly", protocol="redis", port=REDIS_PORT) + ), + DefaultValueValidator("valkey.url", default=fetch_service_ip("valkey", protocol="redis", port=REDIS_PORT)), DefaultValueValidator( "custom_metrics_apiserver.url", - default=fetch_service_ip("custom-metrics-apiserver", protocol="http", port=8080), + default=fetch_service_ip("custom-metrics-apiserver", protocol="http", port=HTTP_API_PORT), ), DefaultValueValidator( "prometheus.url", diff --git a/testsuite/gateway/envoy/__init__.py b/testsuite/gateway/envoy/__init__.py index 4ba29eb8a..8e14c4237 100644 --- a/testsuite/gateway/envoy/__init__.py +++ b/testsuite/gateway/envoy/__init__.py @@ -12,6 +12,14 @@ from testsuite.gateway.envoy.config import EnvoyConfig from testsuite.kubernetes.deployment import Deployment, VolumeMount, ConfigMapVolume from testsuite.kubernetes.service import Service, ServicePort +from testsuite.utils.constants import ( + ENVOY_STARTUP_SETTLE, + GATEWAY_READY_TIMEOUT, + HTTP_API_PORT, + ENVOY_ADMIN_PORT, + ENVOY_READINESS_INITIAL_DELAY, + ENVOY_READINESS_PERIOD, +) class Envoy(Gateway): # pylint: disable=too-many-instance-attributes @@ -54,9 +62,9 @@ def rollout(self): """Restarts Envoy to apply newest config changes""" self.cluster.do_action("rollout", ["restart", f"deployment/{self.name}"]) self.wait_for_ready() - time.sleep(3) # or some reason wait_for_ready is not enough, needs more investigation + time.sleep(ENVOY_STARTUP_SETTLE) # or some reason wait_for_ready is not enough, needs more investigation - def wait_for_ready(self, timeout: int = 10 * 60): + def wait_for_ready(self, timeout: int = GATEWAY_READY_TIMEOUT): with oc.timeout(timeout): assert self.cluster.do_action( "rollout", ["status", f"deployment/{self.name}"] @@ -69,7 +77,7 @@ def create_deployment(self) -> Deployment: self.name, container_name="envoy", image=self.image, - ports={"api": 8080, "admin": 8001}, + ports={"api": HTTP_API_PORT, "admin": ENVOY_ADMIN_PORT}, selector=Selector(matchLabels={"deployment": self.name, **self.labels}), labels=self.labels, command_args=[ @@ -79,7 +87,11 @@ def create_deployment(self) -> Deployment: ], volumes=[ConfigMapVolume(config_map_name=self.name, name="config", items={"envoy.yaml": "envoy.yaml"})], volume_mounts=[VolumeMount(mountPath="/usr/local/etc/envoy", name="config", readOnly=True)], - readiness_probe={"httpGet": {"path": "/ready", "port": 8001}, "initialDelaySeconds": 3, "periodSeconds": 4}, + readiness_probe={ + "httpGet": {"path": "/ready", "port": ENVOY_ADMIN_PORT}, + "initialDelaySeconds": ENVOY_READINESS_INITIAL_DELAY, + "periodSeconds": ENVOY_READINESS_PERIOD, + }, ) def commit(self): @@ -94,7 +106,7 @@ def commit(self): self.cluster, self.name, selector={"deployment": self.name, **self.labels}, - ports=[ServicePort(name="api", port=8080, targetPort="api")], + ports=[ServicePort(name="api", port=HTTP_API_PORT, targetPort="api")], service_type="LoadBalancer", ) self.service.commit() diff --git a/testsuite/gateway/gateway_api/gateway.py b/testsuite/gateway/gateway_api/gateway.py index fc79bc499..d32630764 100644 --- a/testsuite/gateway/gateway_api/gateway.py +++ b/testsuite/gateway/gateway_api/gateway.py @@ -13,6 +13,7 @@ from testsuite.kuadrant.policy import Policy from testsuite.kubernetes.deployment import Deployment from testsuite.utils import check_condition, asdict, domain_match +from testsuite.utils.constants import GATEWAY_READY_TIMEOUT, SLOW_LOADBALANCER_WAIT class KuadrantGateway(KubernetesObject, Gateway): @@ -92,12 +93,12 @@ def is_ready(self): return True return False - def wait_for_ready(self, timeout: int = 10 * 60): + def wait_for_ready(self, timeout: int = GATEWAY_READY_TIMEOUT): """Waits for the gateway to be ready in the sense of is_ready(self)""" success = self.wait_until(lambda obj: self.__class__(obj.model).is_ready(), timelimit=timeout) assert success, f"Gateway didn't reach required state, instead it was: {self.model.status.conditions}" if settings["control_plane"]["slow_loadbalancers"]: - sleep(60) + sleep(SLOW_LOADBALANCER_WAIT) def is_affected_by(self, policy: Policy) -> bool: """Returns True, if affected by status is found within the object for the specific policy""" diff --git a/testsuite/gateway/gateway_api/grpc_route.py b/testsuite/gateway/gateway_api/grpc_route.py index d9fb3b5c7..6ff78706d 100644 --- a/testsuite/gateway/gateway_api/grpc_route.py +++ b/testsuite/gateway/gateway_api/grpc_route.py @@ -7,6 +7,7 @@ from testsuite.kubernetes import KubernetesObject, modify from testsuite.kuadrant.policy import Policy from testsuite.utils import asdict, check_condition +from testsuite.utils.constants import ROUTE_READY_TIMEOUT if typing.TYPE_CHECKING: from testsuite.backend import Backend @@ -125,5 +126,5 @@ def _ready(obj): return all(x.status == "True" for x in condition_set.conditions) return False - success = self.wait_until(_ready, timelimit=10) + success = self.wait_until(_ready, timelimit=ROUTE_READY_TIMEOUT) assert success, f"{self.kind()} did not get ready in time" diff --git a/testsuite/gateway/gateway_api/route.py b/testsuite/gateway/gateway_api/route.py index ddb024346..bc45a8f0a 100644 --- a/testsuite/gateway/gateway_api/route.py +++ b/testsuite/gateway/gateway_api/route.py @@ -8,6 +8,7 @@ from testsuite.gateway import Gateway, GatewayRoute, PathMatch, MatchType, RouteMatch, URLRewriteFilter from testsuite.kubernetes.client import KubernetesClient from testsuite.kubernetes import KubernetesObject, modify +from testsuite.utils.constants import ROUTE_READY_TIMEOUT from testsuite.kuadrant.policy import Policy from testsuite.utils import asdict, check_condition @@ -134,5 +135,5 @@ def _ready(obj): return (all(x.status == "True" for x in condition_set.conditions),) return False - success = self.wait_until(_ready, timelimit=10) + success = self.wait_until(_ready, timelimit=ROUTE_READY_TIMEOUT) assert success, f"{self.kind()} did not get ready in time" diff --git a/testsuite/grpc/__init__.py b/testsuite/grpc/__init__.py index 106db2632..2ab14eae8 100644 --- a/testsuite/grpc/__init__.py +++ b/testsuite/grpc/__init__.py @@ -7,6 +7,7 @@ from testsuite.httpx import ResultList from testsuite.backend.grpc import grpcbin_pb2 +from testsuite.utils.constants import GRPC_CALL_TIMEOUT SERVICE_DESCRIPTOR = grpcbin_pb2.DESCRIPTOR.services_by_name["GRPCBin"] @@ -59,7 +60,7 @@ def call(self, method, *, service="/grpcbin.GRPCBin", auth=None, headers=None): response_deserializer=GetMessageClass(SERVICE_DESCRIPTOR.methods_by_name[method].output_type).FromString, ) try: - response = make_call(grpcbin_pb2.EmptyMessage(), metadata=metadata or None, timeout=10) + response = make_call(grpcbin_pb2.EmptyMessage(), metadata=metadata or None, timeout=GRPC_CALL_TIMEOUT) return GRPCResult(StatusCode.OK, response=response) except grpc.RpcError as e: return GRPCResult(e.code(), error=e) # pylint: disable=no-member diff --git a/testsuite/httpx/__init__.py b/testsuite/httpx/__init__.py index 85fa48234..33de39777 100644 --- a/testsuite/httpx/__init__.py +++ b/testsuite/httpx/__init__.py @@ -23,6 +23,7 @@ ) from testsuite.certificates import Certificate +from testsuite.utils.constants import HTTP_BACKOFF_MAX_RETRIES def create_tmp_file(content: str): @@ -151,7 +152,9 @@ def add_retry_code(self, code): self.retry_codes.add(code) # pylint: disable=too-many-locals - @backoff.on_predicate(backoff.fibo, lambda result: result.should_backoff(), max_tries=8, jitter=None) + @backoff.on_predicate( + backoff.fibo, lambda result: result.should_backoff(), max_tries=HTTP_BACKOFF_MAX_RETRIES, jitter=None + ) def request( self, method: str, diff --git a/testsuite/kuadrant/extensions/oidc_policy.py b/testsuite/kuadrant/extensions/oidc_policy.py index 4ea5b6e42..2896b48c0 100644 --- a/testsuite/kuadrant/extensions/oidc_policy.py +++ b/testsuite/kuadrant/extensions/oidc_policy.py @@ -7,6 +7,7 @@ from testsuite.kubernetes.client import KubernetesClient from testsuite.kuadrant.policy import Policy from testsuite.utils import asdict +from testsuite.utils.constants import OIDC_POST_ENFORCEMENT_WAIT @dataclass @@ -63,4 +64,4 @@ def wait_for_ready(self): """Wait for OIDCPolicy to be enforced""" super().wait_for_ready() # Even after enforced condition OIDCPolicy requires a short sleep - time.sleep(10) # https://github.com/Kuadrant/testsuite/issues/884 + time.sleep(OIDC_POST_ENFORCEMENT_WAIT) # Workaround for issue #884 diff --git a/testsuite/kuadrant/policy/__init__.py b/testsuite/kuadrant/policy/__init__.py index b30c81681..f77efd9c4 100644 --- a/testsuite/kuadrant/policy/__init__.py +++ b/testsuite/kuadrant/policy/__init__.py @@ -5,6 +5,7 @@ from testsuite.kubernetes import KubernetesObject from testsuite.utils import check_condition +from testsuite.utils.constants import POLICY_ENFORCEMENT_TIMEOUT class Strategy(Enum): @@ -90,7 +91,7 @@ def wait_for_partial_enforced(self): ) assert success, f"{self.kind(False)} did not get partially enforced in time" - def wait_for_full_enforced(self, timelimit=60): + def wait_for_full_enforced(self, timelimit=POLICY_ENFORCEMENT_TIMEOUT): """Wait for a Policy to be fully Enforced""" success = self.wait_until( has_condition("Enforced", "True", "Enforced", f"{self.kind(False)} has been successfully enforced"), diff --git a/testsuite/kuadrant/policy/dns.py b/testsuite/kuadrant/policy/dns.py index 80268d5f6..b0a8c4641 100644 --- a/testsuite/kuadrant/policy/dns.py +++ b/testsuite/kuadrant/policy/dns.py @@ -11,6 +11,7 @@ from testsuite.kubernetes.client import KubernetesClient from testsuite.kuadrant.policy import Policy from testsuite.utils import asdict, check_condition +from testsuite.utils.constants import DNS_POLICY_ENFORCEMENT_TIMEOUT def has_record_condition(condition_type, status="True", reason=None, message=None): @@ -199,6 +200,6 @@ def get_dns_health_probe(self) -> DNSHealthCheckProbe: dns_probe = dns_record.get_owned("DNSHealthCheckProbe")[0] return DNSHealthCheckProbe(dns_probe.model, context=self.context) - def wait_for_full_enforced(self, timelimit=300): + def wait_for_full_enforced(self, timelimit=DNS_POLICY_ENFORCEMENT_TIMEOUT): """Wait for a Policy to be fully Enforced with increased timelimit for DNSPolicy""" super().wait_for_full_enforced(timelimit=timelimit) diff --git a/testsuite/kuadrant/policy/rate_limit.py b/testsuite/kuadrant/policy/rate_limit.py index fe465261a..3c8f2afa0 100644 --- a/testsuite/kuadrant/policy/rate_limit.py +++ b/testsuite/kuadrant/policy/rate_limit.py @@ -9,6 +9,7 @@ from testsuite.kubernetes.client import KubernetesClient from testsuite.kuadrant.policy import Policy, CelPredicate, CelExpression, Strategy from testsuite.utils import asdict +from testsuite.utils.constants import RLP_POST_ENFORCEMENT_WAIT @dataclass @@ -114,4 +115,4 @@ def wait_for_ready(self): """Wait for RLP to be enforced""" super().wait_for_ready() # Even after enforced condition RLP requires a short sleep - time.sleep(5) + time.sleep(RLP_POST_ENFORCEMENT_WAIT) diff --git a/testsuite/kuadrant/policy/tls.py b/testsuite/kuadrant/policy/tls.py index aa4a3b5de..786503823 100644 --- a/testsuite/kuadrant/policy/tls.py +++ b/testsuite/kuadrant/policy/tls.py @@ -3,6 +3,7 @@ from testsuite.gateway import Referencable from testsuite.kubernetes.client import KubernetesClient from testsuite.kuadrant.policy import Policy +from testsuite.utils.constants import TLS_POLICY_ENFORCEMENT_TIMEOUT class TLSPolicy(Policy): @@ -49,6 +50,6 @@ def __setitem__(self, key, value): def __getitem__(self, key): return self.model.spec[key] - def wait_for_full_enforced(self, timelimit=450): + def wait_for_full_enforced(self, timelimit=TLS_POLICY_ENFORCEMENT_TIMEOUT): """Wait for a Policy to be fully Enforced with increased timelimit for ACME challenges""" super().wait_for_full_enforced(timelimit=timelimit) diff --git a/testsuite/kubernetes/__init__.py b/testsuite/kubernetes/__init__.py index f77d30735..73a2033b9 100644 --- a/testsuite/kubernetes/__init__.py +++ b/testsuite/kubernetes/__init__.py @@ -7,6 +7,8 @@ from openshift_client import APIObject, timeout, OpenShiftPythonException +from testsuite.utils.constants import K8S_DELETE_TIMEOUT, K8S_WAIT_UNTIL_TIMEOUT + from testsuite.lifecycle import LifecycleObject from testsuite.utils import asdict @@ -53,12 +55,12 @@ def apply(self, modifier_func=None, retries=2, **kwargs): # pylint: disable=arg def delete(self, ignore_not_found=True, cmd_args=None): """Deletes the resource, by default ignored not found""" - with timeout(30): + with timeout(K8S_DELETE_TIMEOUT): deleted = super().delete(ignore_not_found, cmd_args) self._committed = False return deleted - def wait_until(self, test_function, timelimit=60): + def wait_until(self, test_function, timelimit=K8S_WAIT_UNTIL_TIMEOUT): """Waits until the test function succeeds for this object""" try: with timeout(timelimit): diff --git a/testsuite/kubernetes/deployment.py b/testsuite/kubernetes/deployment.py index 712ff883b..c20762fe6 100644 --- a/testsuite/kubernetes/deployment.py +++ b/testsuite/kubernetes/deployment.py @@ -7,6 +7,7 @@ from testsuite.kubernetes import KubernetesObject, Selector, modify from testsuite.utils import asdict +from testsuite.utils.constants import DEPLOYMENT_READY_TIMEOUT # pylint: disable=invalid-name @@ -149,12 +150,12 @@ def create_instance( return cls(model, context=cluster.context) - def wait_for_ready(self, timeout=90): + def wait_for_ready(self, timeout=DEPLOYMENT_READY_TIMEOUT): """Waits until Deployment is marked as ready""" success = self.wait_until(lambda obj: "readyReplicas" in obj.model.status, timelimit=timeout) assert success, f"Deployment {self.name()} did not get ready in time" - def wait_for_replicas(self, replicas: int, timeout=90): + def wait_for_replicas(self, replicas: int, timeout=DEPLOYMENT_READY_TIMEOUT): """Waits until Deployment has at least the given number of replicas""" success = self.wait_until( lambda obj: "readyReplicas" in obj.model.status and obj.model.status["readyReplicas"] >= replicas, @@ -201,7 +202,7 @@ def restart(self): self.set_replicas(original_replicas) self.wait_for_replicas(original_replicas) - def rollout(self, hard=False, timeout=90): + def rollout(self, hard=False, timeout=DEPLOYMENT_READY_TIMEOUT): """ Performs rollout on the Deployment and waits until complete. Setting hard to true performs a direct pod deletion without grace period diff --git a/testsuite/kubernetes/service.py b/testsuite/kubernetes/service.py index dff19d0e1..4440c6d81 100644 --- a/testsuite/kubernetes/service.py +++ b/testsuite/kubernetes/service.py @@ -7,6 +7,7 @@ from openshift_client import timeout, Missing from testsuite.kubernetes import KubernetesObject +from testsuite.utils.constants import SERVICE_DELETE_TIMEOUT, SERVICE_READY_TIMEOUT, SLOW_LOADBALANCER_WAIT @dataclass @@ -73,11 +74,11 @@ def external_ip(self): def delete(self, ignore_not_found=True, cmd_args=None): """Deletes Service, introduces bigger waiting times due to LoadBalancer type""" - with timeout(10 * 60): + with timeout(SERVICE_DELETE_TIMEOUT): deleted = super(KubernetesObject, self).delete(ignore_not_found, cmd_args) return deleted - def wait_for_ready(self, timeout=60 * 5, slow_loadbalancers=False): + def wait_for_ready(self, timeout=SERVICE_READY_TIMEOUT, slow_loadbalancers=False): """Waits until LoadBalancer service gets ready.""" if self.model.spec.type != "LoadBalancer": return @@ -88,4 +89,4 @@ def wait_for_ready(self, timeout=60 * 5, slow_loadbalancers=False): ) assert success, f"Service {self.name()} did not get ready in time" if slow_loadbalancers: - sleep(60) + sleep(SLOW_LOADBALANCER_WAIT) diff --git a/testsuite/prometheus.py b/testsuite/prometheus.py index 2ce6e15c0..912d70ee5 100644 --- a/testsuite/prometheus.py +++ b/testsuite/prometheus.py @@ -10,6 +10,14 @@ from testsuite.kubernetes.monitoring.pod_monitor import PodMonitor from testsuite.kubernetes.monitoring.service_monitor import ServiceMonitor +from testsuite.utils.constants import ( + PROMETHEUS_POLL_INTERVAL, + PROMETHEUS_MAX_RETRIES, + PROMETHEUS_SCRAPE_RETRIES, + PROMETHEUS_METRIC_RETRIES, + PROMETHEUS_FAST_INTERVAL, + PROMETHEUS_VERIFY_NO_TARGETS_RETRIES, +) def _params(key: str = "", labels: dict[str, str] = None) -> dict[str, str]: @@ -75,7 +83,9 @@ def get_metrics(self, key: str = "", labels: dict[str, str] = None) -> Metrics: return Metrics(response.json()["data"]["result"]) - @backoff.on_predicate(backoff.constant, interval=10, jitter=None, max_tries=35) + @backoff.on_predicate( + backoff.constant, interval=PROMETHEUS_POLL_INTERVAL, jitter=None, max_tries=PROMETHEUS_MAX_RETRIES + ) def is_reconciled(self, monitor: ServiceMonitor | PodMonitor): """True, if all endpoints in ServiceMonitor are active targets""" scrape_pools = set(target["scrapePool"].lower() for target in self.get_active_targets()) @@ -95,7 +105,9 @@ def wait_for_scrape(self, monitor: ServiceMonitor | PodMonitor, metrics_path: st """Wait before next metrics scrape on service is finished""" call_time = datetime.now(timezone.utc) - @backoff.on_predicate(backoff.constant, interval=10, jitter=None, max_tries=4) + @backoff.on_predicate( + backoff.constant, interval=PROMETHEUS_POLL_INTERVAL, jitter=None, max_tries=PROMETHEUS_SCRAPE_RETRIES + ) def _wait_for_scrape(): """Wait for new scrape after the function call time""" for target in self.get_active_targets(): @@ -119,7 +131,9 @@ def wait_for_metric( Treats missing metrics as value 0. Supports any comparison via operator module (e.g. operator.eq, operator.ge, operator.lt).""" - @backoff.on_predicate(backoff.constant, interval=10, jitter=None, max_tries=5) + @backoff.on_predicate( + backoff.constant, interval=PROMETHEUS_POLL_INTERVAL, jitter=None, max_tries=PROMETHEUS_METRIC_RETRIES + ) def _wait(): metrics = self.get_metrics(key=metric_name, labels=labels) values = metrics.values @@ -131,7 +145,9 @@ def _wait(): return _wait() - @backoff.on_predicate(backoff.constant, interval=5, max_tries=12, jitter=None) + @backoff.on_predicate( + backoff.constant, interval=PROMETHEUS_FAST_INTERVAL, max_tries=PROMETHEUS_VERIFY_NO_TARGETS_RETRIES, jitter=None + ) def verify_no_observability_targets(self, label_filters: dict[str, list[str]]) -> bool: """Verify that no observability targets are active in Prometheus""" targets = self.get_active_targets() diff --git a/testsuite/spicedb/spicedb.py b/testsuite/spicedb/spicedb.py index ecdbdc031..d7e6e29d1 100644 --- a/testsuite/spicedb/spicedb.py +++ b/testsuite/spicedb/spicedb.py @@ -10,6 +10,14 @@ from testsuite.kubernetes.client import KubernetesClient from testsuite.kubernetes.deployment import Deployment from testsuite.kubernetes.service import Service, ServicePort +from testsuite.utils.constants import ( + SPICEDB_CONNECTION_TIMEOUT, + SPICEDB_RETRY_INTERVAL, + SPICEDB_MAX_RETRIES, + SPICEDB_GRPC_PORT, + SPICEDB_HTTP_PORT, + SERVICE_READY_TIMEOUT, +) @dataclass @@ -69,7 +77,7 @@ def __init__(self, server_url: str, token: str): self.client = KuadrantClient( base_url=self.server_url, headers={"Authorization": f"Bearer {token}"}, - timeout=30.0, + timeout=SPICEDB_CONNECTION_TIMEOUT, ) def create_schema(self, schema_config: SchemaConfig): @@ -139,8 +147,8 @@ def clear_all_relationships(self, schema_config: SchemaConfig): @backoff.on_predicate( backoff.constant, lambda x: x is False, - max_tries=3, - interval=5, + max_tries=SPICEDB_MAX_RETRIES, + interval=SPICEDB_RETRY_INTERVAL, jitter=None, on_giveup=lambda details: (_ for _ in ()).throw( TimeoutError(f"SpiceDB relationships not ready after {details['tries']} tries.") @@ -234,7 +242,7 @@ def commit(self): self.name, container_name="spicedb", image=self.image, - ports={"grpc": 50051, "http": 8443}, + ports={"grpc": SPICEDB_GRPC_PORT, "http": SPICEDB_HTTP_PORT}, selector=Selector(matchLabels=match_labels), labels={"app": self.label}, command_args=[ @@ -254,13 +262,13 @@ def commit(self): self.name, selector=match_labels, ports=[ - ServicePort(name="grpc", port=50051, targetPort="grpc"), - ServicePort(name="http", port=8443, targetPort="http"), + ServicePort(name="grpc", port=SPICEDB_GRPC_PORT, targetPort="grpc"), + ServicePort(name="http", port=SPICEDB_HTTP_PORT, targetPort="http"), ], labels={"app": self.label}, ) self.service.commit() - def wait_for_ready(self, timeout=60 * 5): + def wait_for_ready(self, timeout=SERVICE_READY_TIMEOUT): """Waits until Deployment is marked as ready""" self.deployment.wait_for_ready(timeout) diff --git a/testsuite/tracing/jaeger.py b/testsuite/tracing/jaeger.py index eb48c29e1..754a2be0b 100644 --- a/testsuite/tracing/jaeger.py +++ b/testsuite/tracing/jaeger.py @@ -9,6 +9,7 @@ from testsuite.tracing import TracingClient from testsuite.tracing.models import Trace +from testsuite.utils.constants import TRACING_MAX_RETRIES class JaegerClient(TracingClient): @@ -32,7 +33,7 @@ def collector_url(self): def query_url(self): return self._query_url - @backoff.on_predicate(backoff.fibo, lambda x: x == [], max_tries=7, jitter=None) + @backoff.on_predicate(backoff.fibo, lambda x: x == [], max_tries=TRACING_MAX_RETRIES, jitter=None) def get_traces(self, service: str, tags: Optional[dict[str, str]] = None, min_processes: int = 0) -> list[Trace]: """Gets trace from tracing backend Tempo or Jaeger. If min_processes is set, retries until at least that many service processes are present. diff --git a/testsuite/tracing/tempo.py b/testsuite/tracing/tempo.py index ff53a3246..a4a269e83 100644 --- a/testsuite/tracing/tempo.py +++ b/testsuite/tracing/tempo.py @@ -6,12 +6,13 @@ from testsuite.tracing.jaeger import JaegerClient from testsuite.tracing.models import Trace +from testsuite.utils.constants import TRACING_MAX_RETRIES class RemoteTempoClient(JaegerClient): """Client to a Tempo that is deployed remotely""" - @backoff.on_predicate(backoff.fibo, lambda x: x == [], max_tries=7, jitter=None) + @backoff.on_predicate(backoff.fibo, lambda x: x == [], max_tries=TRACING_MAX_RETRIES, jitter=None) def get_traces(self, service: str, tags: Optional[dict[str, str]] = None, min_processes: int = 0) -> list[Trace]: """Gets trace from Tempo tracing backend. If min_processes is set, retries until at least that many service processes are present""" diff --git a/testsuite/utils.py b/testsuite/utils/__init__.py similarity index 100% rename from testsuite/utils.py rename to testsuite/utils/__init__.py diff --git a/testsuite/utils/constants.py b/testsuite/utils/constants.py new file mode 100644 index 000000000..adf30ad32 --- /dev/null +++ b/testsuite/utils/constants.py @@ -0,0 +1,121 @@ +"""Centralized constants for the Kuadrant test suite.""" + +# --- Kubernetes Resource Timeouts (seconds) --- + +# Additional wait after LoadBalancer IP is assigned on slow cloud providers. +SLOW_LOADBALANCER_WAIT = 60 + +# Default timeout for K8s wait_until condition checks. +K8S_WAIT_UNTIL_TIMEOUT = 60 + +# Timeout for KubernetesObject.delete() operations. +K8S_DELETE_TIMEOUT = 30 + +# Timeout for Deployment readiness and rollout. +DEPLOYMENT_READY_TIMEOUT = 90 + +# Timeout for Service/LoadBalancer readiness. +SERVICE_READY_TIMEOUT = 300 # 5 minutes + +# Timeout for Service deletion (LoadBalancer cleanup can be slow). +SERVICE_DELETE_TIMEOUT = 600 # 10 minutes + +# Timeout for Gateway readiness (programmed status). +GATEWAY_READY_TIMEOUT = 600 # 10 minutes + +# Timeout for Route readiness (controller reconciliation). +ROUTE_READY_TIMEOUT = 10 + +# --- Policy Enforcement Timeouts (seconds) --- + +# Default policy enforcement timeout. +POLICY_ENFORCEMENT_TIMEOUT = 60 + +# DNSPolicy enforcement timeout (DNS propagation is slow). +DNS_POLICY_ENFORCEMENT_TIMEOUT = 300 # 5 minutes + +# TLSPolicy enforcement timeout (includes ACME challenge time). +TLS_POLICY_ENFORCEMENT_TIMEOUT = 450 # 7.5 minutes + +# --- Rate Limiting (seconds) --- + +# Wait after RateLimitPolicy enforcement (enforcer sync delay). +RLP_POST_ENFORCEMENT_WAIT = 5 + +# --- Prometheus & Observability --- + +# Prometheus is_reconciled polling (~350s total). +PROMETHEUS_POLL_INTERVAL = 10 +PROMETHEUS_MAX_RETRIES = 35 + +# Prometheus wait_for_scrape polling (~40s total). +PROMETHEUS_SCRAPE_RETRIES = 4 + +# Prometheus wait_for_metric polling (~50s total). +PROMETHEUS_METRIC_RETRIES = 5 + +# Fast Prometheus polling (~60s total). +PROMETHEUS_FAST_INTERVAL = 5 +PROMETHEUS_VERIFY_NO_TARGETS_RETRIES = 12 + +# Tracing get_traces retry (fibonacci backoff, 7 attempts). +TRACING_MAX_RETRIES = 7 + +# HTTPX request retry (fibonacci backoff, 8 attempts). +HTTP_BACKOFF_MAX_RETRIES = 8 + +# --- SpiceDB --- + +# HTTP client timeout for SpiceDB API calls (seconds). +SPICEDB_CONNECTION_TIMEOUT = 30.0 + +# SpiceDB relationship readiness retry (5s * 3). +SPICEDB_RETRY_INTERVAL = 5 +SPICEDB_MAX_RETRIES = 3 + +# --- Service Ports --- + +# Standard HTTP API port (shared across multiple services). +HTTP_API_PORT = 8080 + +# MockServer container port. +MOCKSERVER_INTERNAL_PORT = 1080 + +# Envoy admin interface. +ENVOY_ADMIN_PORT = 8001 + +# OpenTelemetry gRPC collector (Jaeger). +OTEL_COLLECTOR_PORT = 4317 + +# Redis / Dragonfly / Valkey. +REDIS_PORT = 6379 + +# HashiCorp Vault. +VAULT_PORT = 8200 + +# SpiceDB gRPC. +SPICEDB_GRPC_PORT = 50051 + +# SpiceDB HTTP/TLS. +SPICEDB_HTTP_PORT = 8443 + +# --- gRPC --- + +# Default timeout for individual gRPC unary calls (seconds). +GRPC_CALL_TIMEOUT = 10 + +# --- Envoy Workarounds (seconds) --- + +# Extra wait after envoy rollout (wait_for_ready alone is insufficient). +ENVOY_STARTUP_SETTLE = 3 + +# Envoy readiness probe initial delay. +ENVOY_READINESS_INITIAL_DELAY = 3 + +# Envoy readiness probe period. +ENVOY_READINESS_PERIOD = 4 + +# --- Miscellaneous Workarounds (seconds) --- + +# Workaround for https://github.com/Kuadrant/testsuite/issues/884 — remove when fixed +OIDC_POST_ENFORCEMENT_WAIT = 10