From f9b56eaca83db583e3e12adb67aa69d4c85a4f1e Mon Sep 17 00:00:00 2001 From: averevki Date: Thu, 7 Mar 2024 13:18:48 +0100 Subject: [PATCH 1/2] Add Jaeger client for tracing tests --- config/settings.local.yaml.tpl | 3 +++ testsuite/config/__init__.py | 6 ++++- testsuite/config/tools.py | 25 ++++++++++++++++++++ testsuite/httpx/__init__.py | 3 ++- testsuite/openshift/client.py | 5 ++++ testsuite/tests/conftest.py | 16 +++++++++++++ testsuite/tracing.py | 43 ++++++++++++++++++++++++++++++++++ 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 testsuite/tracing.py diff --git a/config/settings.local.yaml.tpl b/config/settings.local.yaml.tpl index c544ac73..c59510a8 100644 --- a/config/settings.local.yaml.tpl +++ b/config/settings.local.yaml.tpl @@ -21,6 +21,9 @@ # url: "AUTH0_URL" # mockserver: # url: "MOCKSERVER_URL" +# tracing: +# collector_url: "rpc://jaeger-collector.com:4317" # Tracing collector URL (may be internal) +# query_url: "http://jaeger-query.com" # Tracing query URL # cfssl: "cfssl" # Path to the CFSSL library for TLS tests # hyperfoil: # url: "HYPERFOIL_URL" diff --git a/testsuite/config/__init__.py b/testsuite/config/__init__.py index 95841330..63a2cd9f 100644 --- a/testsuite/config/__init__.py +++ b/testsuite/config/__init__.py @@ -2,7 +2,7 @@ from dynaconf import Dynaconf, Validator -from testsuite.config.tools import fetch_route, fetch_secret +from testsuite.config.tools import fetch_route, fetch_service, fetch_secret # pylint: disable=too-few-public-methods @@ -38,6 +38,10 @@ def __init__(self, name, default, **kwargs) -> None: Validator("service_protection.authorino.auth_url", must_exist=True) & Validator("service_protection.authorino.oidc_url", must_exist=True) ), + DefaultValueValidator( + "tracing.collector_url", default=fetch_service("jaeger-collector", protocol="rpc", port=4317) + ), + DefaultValueValidator("tracing.query_url", default=fetch_route("jaeger-query", force_http=True)), DefaultValueValidator("rhsso.url", default=fetch_route("no-ssl-sso")), DefaultValueValidator("rhsso.password", default=fetch_secret("credential-sso", "ADMIN_PASSWORD")), DefaultValueValidator("mockserver.url", default=fetch_route("mockserver", force_http=True)), diff --git a/testsuite/config/tools.py b/testsuite/config/tools.py index 95cd2dfd..3894d9bb 100644 --- a/testsuite/config/tools.py +++ b/testsuite/config/tools.py @@ -23,6 +23,31 @@ def _fetcher(settings, _): return _fetcher +def fetch_service(name, protocol: str = None, port: int = None): + """Fetches the local URL of existing service with specific name""" + + def _fetcher(settings, _): + openshift = settings["tools"] + try: + if not openshift.service_exists(name): + logger.warning("Unable to fetch service %s from tools, service does not exists", name) + return None + except AttributeError: + logger.warning("Unable to fetch service %s from tools, tools project might be missing", name) + return None + + service_url = f"{name}.{openshift.project}.svc.cluster.local" + + if protocol: + service_url = f"{protocol}://{service_url}" + if port: + service_url = f"{service_url}:{port}" + + return service_url + + return _fetcher + + def fetch_secret(name, key): """Fetches the key out of a secret with specific name""" diff --git a/testsuite/httpx/__init__.py b/testsuite/httpx/__init__.py index ae070c0b..01cffaff 100644 --- a/testsuite/httpx/__init__.py +++ b/testsuite/httpx/__init__.py @@ -97,7 +97,8 @@ def __init__(self, *, verify: Union[Certificate, bool] = True, cert: Certificate _cert = (cert_file.name, key_file.name) # Mypy does not understand the typing magic I have done - super().__init__(verify=_verify or verify, cert=_cert or cert, **kwargs) # type: ignore + self.verify = _verify or verify + super().__init__(verify=self.verify, cert=_cert or cert, **kwargs) # type: ignore def close(self) -> None: super().close() diff --git a/testsuite/openshift/client.py b/testsuite/openshift/client.py index dbbd42ac..97e107b8 100644 --- a/testsuite/openshift/client.py +++ b/testsuite/openshift/client.py @@ -83,6 +83,11 @@ def get_secret(self, name): with self.context: return oc.selector(f"secret/{name}").object(cls=Secret) + def service_exists(self, name) -> bool: + """Returns True if service with the given name exists""" + with self.context: + return oc.selector(f"svc/{name}").count_existing() == 1 + def get_route(self, name): """Returns dict-like structure for accessing secret data""" with self.context: diff --git a/testsuite/tests/conftest.py b/testsuite/tests/conftest.py index 83e06199..2dfb7b60 100644 --- a/testsuite/tests/conftest.py +++ b/testsuite/tests/conftest.py @@ -10,7 +10,9 @@ from testsuite.capabilities import has_kuadrant, has_mgc from testsuite.certificates import CFSSLClient from testsuite.config import settings +from testsuite.httpx import KuadrantClient from testsuite.mockserver import Mockserver +from testsuite.tracing import TracingClient from testsuite.gateway import Gateway, GatewayRoute, Hostname, Exposer from testsuite.oidc import OIDCProvider from testsuite.oidc.auth0 import Auth0Provider @@ -203,6 +205,20 @@ def mockserver(testconfig, skip_or_fail): return skip_or_fail(f"Mockserver configuration item is missing: {exc}") +@pytest.fixture(scope="module") +def tracing(testconfig, skip_or_fail): + """Returns tracing client for tracing tests""" + try: + testconfig.validators.validate(only=["tracing"]) + return TracingClient( + testconfig["tracing"]["collector_url"], + testconfig["tracing"]["query_url"], + KuadrantClient(verify=False), + ) + except (KeyError, ValidationError) as exc: + return skip_or_fail(f"Tracing configuration item is missing: {exc}") + + @pytest.fixture(scope="session") def oidc_provider(rhsso) -> OIDCProvider: """Fixture which enables switching out OIDC providers for individual modules""" diff --git a/testsuite/tracing.py b/testsuite/tracing.py new file mode 100644 index 00000000..5c13d8e4 --- /dev/null +++ b/testsuite/tracing.py @@ -0,0 +1,43 @@ +"""Module with Tracing client for traces management""" + +from typing import Optional, Iterator + +import backoff +from apyproxy import ApyProxy + +from testsuite.httpx import KuadrantClient + + +class TracingClient: + """Tracing client for traces management""" + + def __init__(self, collector_url: str, query_url: str, client: KuadrantClient = None): + self.collector_url = collector_url + self.client = client or KuadrantClient(verify=False) + self.query = ApyProxy(query_url, session=self.client) + + def _get_traces(self, operation: str) -> Iterator[dict]: + """Get traces from tracing client by operation name""" + params = {"service": "authorino", "operation": operation} + response = self.query.api.traces.get(params=params) + return reversed(response.json()["data"]) + + @backoff.on_predicate(backoff.fibo, lambda x: x is None, max_tries=5, jitter=None) + def find_trace(self, operation: str, request_id: str) -> Optional[dict]: + """Find trace in tracing client by operation and authorino request id""" + for trace in self._get_traces(operation): # pylint: disable=too-many-nested-blocks + for span in trace["spans"]: + if span["operationName"] == operation: + for tag in span["tags"]: + if tag["key"] == "authorino.request_id" and tag["value"] == request_id: + return trace + return None + + def find_tagged_trace(self, operation: str, request_id: str, tag_key: str, tag_value: str) -> Optional[dict]: + """Find trace in tracing client by operation, authorino request id and tag key-value pair""" + if trace := self.find_trace(operation, request_id): + for process in trace["processes"]: + for proc_tag in trace["processes"][process]["tags"]: + if proc_tag["key"] == tag_key and proc_tag["value"] == tag_value: + return trace + return None From 342c30502cd330eead4ceb317595eb9bef906c9a Mon Sep 17 00:00:00 2001 From: averevki Date: Thu, 7 Mar 2024 13:19:03 +0100 Subject: [PATCH 2/2] Add authorino tracing tests --- testsuite/openshift/authorino.py | 17 ++++++++++- .../kuadrant/authorino/tracing/__init__.py | 0 .../kuadrant/authorino/tracing/conftest.py | 20 +++++++++++++ .../authorino/tracing/test_tracing.py | 18 +++++++++++ .../authorino/tracing/test_tracing_tags.py | 30 +++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 testsuite/tests/kuadrant/authorino/tracing/__init__.py create mode 100644 testsuite/tests/kuadrant/authorino/tracing/conftest.py create mode 100644 testsuite/tests/kuadrant/authorino/tracing/test_tracing.py create mode 100644 testsuite/tests/kuadrant/authorino/tracing/test_tracing_tags.py diff --git a/testsuite/openshift/authorino.py b/testsuite/openshift/authorino.py index 9b7b1857..7656e3cd 100644 --- a/testsuite/openshift/authorino.py +++ b/testsuite/openshift/authorino.py @@ -1,13 +1,24 @@ """Authorino CR object""" import abc -from typing import Any, Dict, List +from typing import Any, Optional, Dict, List +from dataclasses import dataclass from openshift_client import selector, timeout from testsuite.openshift.client import OpenShiftClient from testsuite.openshift import OpenShiftObject from testsuite.lifecycle import LifecycleObject +from testsuite.utils import asdict + + +@dataclass +class TracingOptions: + """Dataclass containing authorino tracing specification""" + + endpoint: str + tags: Optional[dict[str, str]] = None + insecure: Optional[bool] = None class Authorino(LifecycleObject): @@ -45,6 +56,7 @@ def create_instance( cluster_wide=False, label_selectors: List[str] = None, listener_certificate_secret=None, + tracing: TracingOptions = None, log_level=None, ): """Creates base instance""" @@ -68,6 +80,9 @@ def create_instance( if listener_certificate_secret: model["spec"]["listener"]["tls"] = {"enabled": True, "certSecretRef": {"name": listener_certificate_secret}} + if tracing: + model["spec"]["tracing"] = asdict(tracing) + with openshift.context: return cls(model) diff --git a/testsuite/tests/kuadrant/authorino/tracing/__init__.py b/testsuite/tests/kuadrant/authorino/tracing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/tests/kuadrant/authorino/tracing/conftest.py b/testsuite/tests/kuadrant/authorino/tracing/conftest.py new file mode 100644 index 00000000..7f519fd9 --- /dev/null +++ b/testsuite/tests/kuadrant/authorino/tracing/conftest.py @@ -0,0 +1,20 @@ +"""Conftest for tracing tests""" + +import pytest + +from testsuite.openshift.authorino import TracingOptions + + +@pytest.fixture(scope="module") +def authorino_parameters(authorino_parameters, tracing): + """Deploy authorino with tracing enabled""" + insecure_tracing = not tracing.client.verify + authorino_parameters["tracing"] = TracingOptions(endpoint=tracing.collector_url, insecure=insecure_tracing) + return authorino_parameters + + +@pytest.fixture(scope="module") +def authorization(authorization): + """Add response with 'request.id' to found traced request with it""" + authorization.responses.add_simple("request.id") + return authorization diff --git a/testsuite/tests/kuadrant/authorino/tracing/test_tracing.py b/testsuite/tests/kuadrant/authorino/tracing/test_tracing.py new file mode 100644 index 00000000..e15a09bf --- /dev/null +++ b/testsuite/tests/kuadrant/authorino/tracing/test_tracing.py @@ -0,0 +1,18 @@ +"""Test tracing""" + +import pytest + +from testsuite.utils import extract_response + +pytestmark = [pytest.mark.authorino, pytest.mark.standalone_only] + + +def test_tracing(client, auth, tracing): + """Send request and check if it's trace is saved into the tracing client""" + response = client.get("/get", auth=auth) + assert response.status_code == 200 + + request_id = extract_response(response) % None + assert request_id is not None + + assert tracing.find_trace("Check", request_id) diff --git a/testsuite/tests/kuadrant/authorino/tracing/test_tracing_tags.py b/testsuite/tests/kuadrant/authorino/tracing/test_tracing_tags.py new file mode 100644 index 00000000..53da9b16 --- /dev/null +++ b/testsuite/tests/kuadrant/authorino/tracing/test_tracing_tags.py @@ -0,0 +1,30 @@ +"""Test custom tags set for request traces""" + +import pytest + +from testsuite.utils import extract_response + +pytestmark = [pytest.mark.authorino, pytest.mark.standalone_only] + + +TAG_KEY = "test-key" +TAG_VALUE = "test-value" + + +@pytest.fixture(scope="module") +def authorino_parameters(authorino_parameters): + """Deploy authorino with tracing enabled and custom tags set""" + authorino_parameters["tracing"].tags = {TAG_KEY: TAG_VALUE} + return authorino_parameters + + +@pytest.mark.issue("https://github.com/Kuadrant/authorino-operator/issues/171") +def test_tracing_tags(client, auth, tracing): + """Send request and check if it's trace with custom tags is saved into the tracing client""" + response = client.get("/get", auth=auth) + assert response.status_code == 200 + + request_id = extract_response(response) % None + assert request_id is not None + + assert tracing.find_tagged_trace("Check", request_id, TAG_KEY, TAG_VALUE)