From 60b4d7f7e87978478beb231a30ddfe029e823cac Mon Sep 17 00:00:00 2001 From: phala Date: Tue, 26 Sep 2023 13:13:45 +0200 Subject: [PATCH] Refactor testsuite to be more aligned with Gateway API --- testsuite/objects/gateway.py | 105 +++++++++++ testsuite/objects/hostname.py | 33 ++++ testsuite/openshift/envoy.py | 114 ------------ testsuite/openshift/httpbin.py | 2 +- testsuite/openshift/objects/__init__.py | 9 +- .../openshift/objects/auth_config/__init__.py | 11 +- .../objects/auth_config/auth_policy.py | 31 +-- testsuite/openshift/objects/dnspolicy.py | 2 +- testsuite/openshift/objects/envoy/__init__.py | 85 +++++++++ testsuite/openshift/objects/envoy/config.py | 130 +++++++++++++ testsuite/openshift/objects/envoy/route.py | 60 ++++++ testsuite/openshift/objects/envoy/tls.py | 85 +++++++++ .../openshift/objects/envoy/wristband.py | 32 ++++ .../openshift/objects/gateway_api/__init__.py | 37 ---- .../openshift/objects/gateway_api/gateway.py | 138 ++++---------- .../openshift/objects/gateway_api/hostname.py | 71 +++++++ .../openshift/objects/gateway_api/route.py | 69 +++---- testsuite/openshift/objects/proxy.py | 13 -- testsuite/openshift/objects/rate_limit.py | 2 +- testsuite/openshift/objects/route.py | 17 +- testsuite/openshift/objects/tlspolicy.py | 2 +- testsuite/resources/envoy.yaml | 106 +---------- testsuite/resources/tls/envoy.yaml | 124 +----------- testsuite/resources/wristband/__init__.py | 0 testsuite/resources/wristband/envoy.yaml | 176 ------------------ testsuite/tests/conftest.py | 80 +++++--- .../kuadrant/authorino/dinosaur/conftest.py | 17 +- .../identity/api_key/test_auth_credentials.py | 4 +- .../identity/rhsso/test_auth_credentials.py | 4 +- .../authorino/multiple_hosts/conftest.py | 18 +- .../multiple_hosts/test_remove_host.py | 4 +- .../operator/clusterwide/conftest.py | 13 +- .../clusterwide/test_wildcard_collision.py | 20 +- .../authorino/operator/http/conftest.py | 4 +- .../authorino/operator/http/test_raw_http.py | 4 +- .../authorino/operator/sharding/conftest.py | 53 ++++-- .../sharding/test_preexisting_auth.py | 52 +++--- .../operator/sharding/test_sharding.py | 19 +- .../authorino/operator/test_wildcard.py | 15 +- .../authorino/operator/tls/conftest.py | 25 ++- .../operator/tls/mtls/test_mtls_attributes.py | 8 +- .../operator/tls/mtls/test_mtls_identity.py | 12 +- .../tls/mtls/test_mtls_trust_chain.py | 12 +- .../authorino/operator/tls/test_tls.py | 12 +- .../kuadrant/authorino/wristband/conftest.py | 37 ++-- testsuite/tests/kuadrant/conftest.py | 8 - .../reconciliation/test_httproute_delete.py | 4 +- .../reconciliation/test_httproute_hosts.py | 15 +- .../reconciliation/test_httproute_matches.py | 4 +- testsuite/tests/mgc/conftest.py | 96 ++++------ testsuite/tests/mgc/dnspolicy/conftest.py | 12 +- .../mgc/dnspolicy/health_check/conftest.py | 27 ++- testsuite/tests/mgc/test_basic.py | 19 +- .../mgc/tlspolicy/test_cert_parameters.py | 4 +- 54 files changed, 1019 insertions(+), 1037 deletions(-) create mode 100644 testsuite/objects/gateway.py create mode 100644 testsuite/objects/hostname.py delete mode 100644 testsuite/openshift/envoy.py create mode 100644 testsuite/openshift/objects/envoy/__init__.py create mode 100644 testsuite/openshift/objects/envoy/config.py create mode 100644 testsuite/openshift/objects/envoy/route.py create mode 100644 testsuite/openshift/objects/envoy/tls.py create mode 100644 testsuite/openshift/objects/envoy/wristband.py create mode 100644 testsuite/openshift/objects/gateway_api/hostname.py delete mode 100644 testsuite/openshift/objects/proxy.py delete mode 100644 testsuite/resources/wristband/__init__.py delete mode 100644 testsuite/resources/wristband/envoy.yaml diff --git a/testsuite/objects/gateway.py b/testsuite/objects/gateway.py new file mode 100644 index 00000000..cf37fa54 --- /dev/null +++ b/testsuite/objects/gateway.py @@ -0,0 +1,105 @@ +"""Module containing Proxy related stuff""" +from abc import abstractmethod, ABC +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional, Any + +from . import LifecycleObject, asdict +from ..certificates import Certificate + +if TYPE_CHECKING: + from testsuite.openshift.client import OpenShiftClient + from testsuite.openshift.httpbin import Httpbin + + +class Referencable(ABC): + """Object that can be referenced in Gateway API style""" + + @property + @abstractmethod + def reference(self) -> dict[str, str]: + """ + Returns dict, which can be used as reference in Gateway API Objects. + https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference + """ + + +@dataclass +class CustomReference(Referencable): + """ + Manually creates Reference object. + https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.ParentReference + """ + + @property + def reference(self) -> dict[str, Any]: + return asdict(self) + + group: str + kind: str + name: str + namespace: Optional[str] = None + sectionName: Optional[str] = None # pylint: disable=invalid-name + port: Optional[int] = None + + +class Gateway(LifecycleObject, Referencable): + """ + Abstraction layer for a Gateway sitting between end-user and Kuadrant + Simplified: Equals to Gateway Kubernetes object + """ + + @property + @abstractmethod + def openshift(self) -> "OpenShiftClient": + """Returns OpenShift client for this gateway""" + + @property + @abstractmethod + def service_name(self) -> str: + """Service name for this gateway""" + + @abstractmethod + def wait_for_ready(self, timeout: int = 90): + """Waits until the gateway is ready""" + + @abstractmethod + def get_tls_cert(self) -> Optional[Certificate]: + """Returns TLS cert bound to this Gateway, if the Gateway does not use TLS, returns None""" + + +class GatewayRoute(LifecycleObject, Referencable): + """ + Abstraction layer for *Route objects in Gateway API + Simplified: Equals to HTTPRoute Kubernetes object + """ + + @classmethod + @abstractmethod + def create_instance( + cls, + openshift: "OpenShiftClient", + name, + gateway: Gateway, + labels: dict[str, str] = None, + ): + """Creates new gateway instance""" + + @abstractmethod + def add_hostname(self, hostname: str): + """Adds hostname to the Route""" + + @abstractmethod + def remove_hostname(self, hostname: str): + """Remove hostname from the Route""" + + @abstractmethod + def remove_all_hostnames(self): + """Remove all hostnames from the Route""" + + @abstractmethod + def add_backend(self, backend: "Httpbin", prefix): + """Adds another backend to the Route, with specific prefix""" + + @abstractmethod + def remove_all_backend(self): + """Sets match for a specific backend""" diff --git a/testsuite/objects/hostname.py b/testsuite/objects/hostname.py new file mode 100644 index 00000000..4d9babdf --- /dev/null +++ b/testsuite/objects/hostname.py @@ -0,0 +1,33 @@ +"""Abstract classes for Hostname related stuff""" +from abc import ABC, abstractmethod + +from httpx import Client + +from .gateway import Gateway + + +class Hostname(ABC): + """ + Abstraction layer on top of externally exposed hostname + Simplified: Does not have any equal Kubernetes object. It is a hostname you can send HTTP request to + """ + + @abstractmethod + def client(self, **kwargs) -> Client: + """Return Httpx client for the requests to this backend""" + + @property + @abstractmethod + def hostname(self) -> str: + """Returns full hostname in string form associated with this object""" + + +class Exposer: + """Exposes hostnames to be accessible from outside""" + + @abstractmethod + def expose_hostname(self, name, gateway: Gateway) -> Hostname: + """ + Exposes hostname, so it is accessible from outside + Actual hostname is generated from "name" and is returned in a form of a Hostname object + """ diff --git a/testsuite/openshift/envoy.py b/testsuite/openshift/envoy.py deleted file mode 100644 index 478b211b..00000000 --- a/testsuite/openshift/envoy.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Module containing all Envoy Classes""" -from importlib import resources - -from openshift import Selector - -from testsuite.openshift.client import OpenShiftClient -from testsuite.openshift.httpbin import Httpbin -from testsuite.openshift.objects.proxy import Proxy -from testsuite.openshift.objects.route import OpenshiftRoute - - -# pylint: disable=too-many-instance-attributes -class Envoy(Proxy): - """Envoy deployed from template""" - - def __init__( - self, openshift: OpenShiftClient, authorino, name, label, httpbin: Httpbin, image, template=None - ) -> None: - self.openshift = openshift - self.authorino = authorino - self.name = name - self.label = label - self.httpbin_hostname = httpbin.url - self.image = image - self.template = template or resources.files("testsuite.resources").joinpath("envoy.yaml") - - self.envoy_objects: Selector = None # type: ignore - - def expose_hostname(self, name) -> OpenshiftRoute: - """Add another hostname that points to this Envoy""" - route = OpenshiftRoute.create_instance( - self.openshift, name, service_name=self.name, target_port="web", labels={"app": self.label} - ) - route.commit() - with self.openshift.context: - self.envoy_objects = self.envoy_objects.union(route.self_selector()) - return route - - def commit(self): - """Deploy all required objects into OpenShift""" - self.envoy_objects = self.openshift.new_app( - self.template, - { - "NAME": self.name, - "LABEL": self.label, - "AUTHORINO_URL": self.authorino.authorization_url, - "UPSTREAM_URL": self.httpbin_hostname, - "ENVOY_IMAGE": self.image, - }, - ) - with self.openshift.context: - assert self.openshift.is_ready(self.envoy_objects.narrow("deployment")), "Envoy wasn't ready in time" - - def delete(self): - """Destroy all objects this instance created""" - with self.openshift.context: - if self.envoy_objects: - self.envoy_objects.delete() - self.envoy_objects = None - - -class TLSEnvoy(Envoy): - """Envoy with TLS enabled and all required certificates set up, requires using a client certificate""" - - def __init__( - self, - openshift, - authorino, - name, - label, - httpbin_hostname, - image, - authorino_ca_secret, - envoy_ca_secret, - envoy_cert_secret, - ) -> None: - super().__init__(openshift, authorino, name, label, httpbin_hostname, image) - self.authorino_ca_secret = authorino_ca_secret - self.backend_ca_secret = envoy_ca_secret - self.envoy_cert_secret = envoy_cert_secret - - def expose_hostname(self, name) -> OpenshiftRoute: - """Add another hostname that points to this Envoy""" - route = OpenshiftRoute.create_instance( - self.openshift, - name, - service_name=self.name, - target_port="web", - labels={"app": self.label}, - tls=True, - termination="passthrough", - ) - route.commit() - with self.openshift.context: - self.envoy_objects = self.envoy_objects.union(route.self_selector()) - return route - - def commit(self): - self.envoy_objects = self.openshift.new_app( - resources.files("testsuite.resources.tls").joinpath("envoy.yaml"), - { - "NAME": self.name, - "LABEL": self.label, - "AUTHORINO_URL": self.authorino.authorization_url, - "UPSTREAM_URL": self.httpbin_hostname, - "AUTHORINO_CA_SECRET": self.authorino_ca_secret, - "ENVOY_CA_SECRET": self.backend_ca_secret, - "ENVOY_CERT_SECRET": self.envoy_cert_secret, - "ENVOY_IMAGE": self.image, - }, - ) - - with self.openshift.context: - assert self.openshift.is_ready(self.envoy_objects.narrow("deployment")), "Envoy wasn't ready in time" diff --git a/testsuite/openshift/httpbin.py b/testsuite/openshift/httpbin.py index 091e8a5a..270fe6b2 100644 --- a/testsuite/openshift/httpbin.py +++ b/testsuite/openshift/httpbin.py @@ -3,8 +3,8 @@ from importlib import resources from testsuite.objects import LifecycleObject +from testsuite.objects.gateway import Referencable from testsuite.openshift.client import OpenShiftClient -from testsuite.openshift.objects.gateway_api import Referencable class Httpbin(LifecycleObject, Referencable): diff --git a/testsuite/openshift/objects/__init__.py b/testsuite/openshift/objects/__init__.py index 2e827394..2c323e86 100644 --- a/testsuite/openshift/objects/__init__.py +++ b/testsuite/openshift/objects/__init__.py @@ -1,7 +1,7 @@ """Base classes/methods for our own APIObjects""" import functools -from openshift import APIObject +from openshift import APIObject, timeout from testsuite.objects import LifecycleObject @@ -48,6 +48,7 @@ def commit(self): def delete(self, ignore_not_found=True, cmd_args=None): """Deletes the resource, by default ignored not found""" - deleted = super().delete(ignore_not_found, cmd_args) - self.committed = False - return deleted + with timeout(30): + deleted = super().delete(ignore_not_found, cmd_args) + self.committed = False + return deleted diff --git a/testsuite/openshift/objects/auth_config/__init__.py b/testsuite/openshift/objects/auth_config/__init__.py index 5e1e5499..802e507e 100644 --- a/testsuite/openshift/objects/auth_config/__init__.py +++ b/testsuite/openshift/objects/auth_config/__init__.py @@ -6,7 +6,6 @@ from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.objects import OpenShiftObject, modify from .sections import IdentitySection, MetadataSection, ResponseSection, AuthorizationSection -from ..route import Route class AuthConfig(OpenShiftObject): @@ -42,19 +41,19 @@ def create_instance( cls, openshift: OpenShiftClient, name, - route: Optional[Route], + route, labels: Dict[str, str] = None, - hostnames: List[str] = None, ): """Creates base instance""" model: Dict = { "apiVersion": "authorino.kuadrant.io/v1beta2", "kind": "AuthConfig", "metadata": {"name": name, "namespace": openshift.project, "labels": labels}, - "spec": {"hosts": hostnames or [route.hostname]}, # type: ignore + "spec": {"hosts": []}, } - - return cls(model, context=openshift.context) + obj = cls(model, context=openshift.context) + route.add_auth_config(obj) + return obj @modify def add_host(self, hostname): diff --git a/testsuite/openshift/objects/auth_config/auth_policy.py b/testsuite/openshift/objects/auth_config/auth_policy.py index 6ba87e15..d7ec82a1 100644 --- a/testsuite/openshift/objects/auth_config/auth_policy.py +++ b/testsuite/openshift/objects/auth_config/auth_policy.py @@ -1,29 +1,16 @@ """Module containing classes related to Auth Policy""" -from typing import Dict, List +from typing import Dict from testsuite.objects import Rule, asdict - +from testsuite.objects.gateway import Referencable from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.objects import modify from testsuite.openshift.objects.auth_config import AuthConfig -from testsuite.openshift.objects.gateway_api.route import HTTPRoute class AuthPolicy(AuthConfig): """AuthPolicy object, it serves as Kuadrants AuthConfig""" - def __init__(self, dict_to_model=None, string_to_model=None, context=None, route: HTTPRoute = None): - super().__init__(dict_to_model, string_to_model, context) - self._route = route - - @property - def route(self) -> HTTPRoute: - """Returns route to which the Policy is bound, won't work with objects fetched from Openshift""" - if not self._route: - # TODO: Fetch route from Openshift directly - raise ValueError("This instance doesnt have a Route specified!!") - return self._route - @property def auth_section(self): return self.model.spec.setdefault("rules", {}) @@ -34,9 +21,8 @@ def create_instance( # type: ignore cls, openshift: OpenShiftClient, name, - route: HTTPRoute, + route: Referencable, labels: Dict[str, str] = None, - hostnames: List[str] = None, ): """Creates base instance""" model: Dict = { @@ -48,16 +34,7 @@ def create_instance( # type: ignore }, } - return cls(model, context=openshift.context, route=route) - - def add_host(self, hostname): - return self.route.add_hostname(hostname) - - def remove_host(self, hostname): - return self.route.remove_hostname(hostname) - - def remove_all_hosts(self): - return self.route.remove_all_hostnames() + return cls(model, context=openshift.context) @modify def add_rule(self, when: list[Rule]): diff --git a/testsuite/openshift/objects/dnspolicy.py b/testsuite/openshift/objects/dnspolicy.py index f45da305..2d59b77e 100644 --- a/testsuite/openshift/objects/dnspolicy.py +++ b/testsuite/openshift/objects/dnspolicy.py @@ -5,9 +5,9 @@ import openshift as oc from testsuite.objects import asdict +from testsuite.objects.gateway import Referencable from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.objects import OpenShiftObject -from testsuite.openshift.objects.gateway_api import Referencable @dataclass diff --git a/testsuite/openshift/objects/envoy/__init__.py b/testsuite/openshift/objects/envoy/__init__.py new file mode 100644 index 00000000..5da5fe34 --- /dev/null +++ b/testsuite/openshift/objects/envoy/__init__.py @@ -0,0 +1,85 @@ +"""Envoy Gateway module""" +import time +from importlib import resources +from typing import Optional + +import openshift as oc + +from testsuite.certificates import Certificate +from testsuite.objects.gateway import Gateway +from testsuite.openshift.client import OpenShiftClient +from testsuite.openshift.objects.envoy.config import EnvoyConfig + + +class Envoy(Gateway): # pylint: disable=too-many-instance-attributes + """Envoy deployed from template""" + + @property + def reference(self) -> dict[str, str]: + raise AttributeError("Not Supported for Envoy-only deployment") + + def __init__(self, openshift: "OpenShiftClient", name, authorino, image, labels: dict[str, str]) -> None: + self._openshift = openshift + self.authorino = authorino + self.name = name + self.image = image + self.labels = labels + self.template = resources.files("testsuite.resources").joinpath("envoy.yaml") + self._config = None + self.envoy_objects: oc.Selector = None # type: ignore + + @property + def config(self): + """Returns EnvoyConfig instance""" + if not self._config: + self._config = EnvoyConfig.create_instance(self.openshift, self.name, self.authorino, self.labels) + return self._config + + @property + def app_label(self): + """Returns App label that should be applied to all resources""" + return self.labels.get("app") + + @property + def openshift(self) -> "OpenShiftClient": + return self._openshift + + @property + def service_name(self) -> str: + return self.name + + def rollout(self): + """Restarts Envoy to apply newest config changes""" + self.openshift.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 + + def wait_for_ready(self, timeout: int = 180): + with oc.timeout(timeout): + assert self.openshift.do_action( + "rollout", ["status", f"deployment/{self.name}"] + ), "Envoy wasn't ready in time" + + def commit(self): + """Deploy all required objects into OpenShift""" + self.config.commit() + self.envoy_objects = self.openshift.new_app( + self.template, + { + "NAME": self.name, + "LABEL": self.app_label, + "ENVOY_IMAGE": self.image, + }, + ) + + def get_tls_cert(self) -> Optional[Certificate]: + return None + + def delete(self): + """Destroy all objects this instance created""" + self.config.delete() + self._config = None + with self.openshift.context: + if self.envoy_objects: + self.envoy_objects.delete() + self.envoy_objects = None diff --git a/testsuite/openshift/objects/envoy/config.py b/testsuite/openshift/objects/envoy/config.py new file mode 100644 index 00000000..5a66c8e4 --- /dev/null +++ b/testsuite/openshift/objects/envoy/config.py @@ -0,0 +1,130 @@ +"""Envoy Config""" +import yaml + +from testsuite.openshift.httpbin import Httpbin +from testsuite.openshift.objects import modify +from testsuite.openshift.objects.config_map import ConfigMap + + +BASE_CONFIG = """ + static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: local + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ['*'] + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + check_settings: + context_extensions: + virtual_host: local_service + routes: [] + http_filters: + - name: envoy.filters.http.ext_authz + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + transport_api_version: V3 + failure_mode_allow: false + status_on_error: {code: 500} + include_peer_certificate: true + grpc_service: + envoy_grpc: + cluster_name: external_auth + timeout: 1s + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + use_remote_address: true + clusters: + - name: external_auth + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: external_auth + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ${authorino_url} + port_value: 50051 + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + upstream_http_protocol_options: + auto_sni: true + explicit_http_config: + http2_protocol_options: {} + admin: + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 +""" + +CLUSTER = """ +name: ${backend_url} +connect_timeout: 0.25s +type: strict_dns +lb_policy: round_robin +load_assignment: + cluster_name: ${backend_url} + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ${backend_url} + port_value: 8080 +""" + + +class EnvoyConfig(ConfigMap): + """ConfigMap containing Envoy configuration""" + + @classmethod + def create_instance( + cls, openshift, name, authorino, labels: dict[str, str] = None + ): # pylint: disable=arguments-renamed + return super().create_instance( + openshift, + name, + {"envoy.yaml": BASE_CONFIG.replace("${authorino_url}", authorino.authorization_url)}, + labels, + ) + + @modify + def add_backend(self, backend: Httpbin, prefix: str): + """Adds backend to the EnvoyConfig""" + config = yaml.safe_load(self["envoy.yaml"]) + config["static_resources"]["clusters"].append(yaml.safe_load(CLUSTER.replace("${backend_url}", backend.url))) + config["static_resources"]["listeners"][0]["filter_chains"][0]["filters"][0]["typed_config"]["route_config"][ + "virtual_hosts" + ][0]["routes"].append({"match": {"prefix": prefix}, "route": {"cluster": backend.url}}) + self["envoy.yaml"] = yaml.dump(config) + + @modify + def remove_all_backends(self): + """Removes all backends from EnvoyConfig""" + config = yaml.safe_load(self["envoy.yaml"]) + clusters = config["static_resources"]["clusters"] + for cluster in clusters: + if cluster["name"] != "external_auth": + clusters.remove(cluster) + config["static_resources"]["listeners"][0]["filter_chains"][0]["filters"][0]["typed_config"]["route_config"][ + "virtual_hosts" + ][0]["routes"] = {} + self["envoy.yaml"] = yaml.dump(config) diff --git a/testsuite/openshift/objects/envoy/route.py b/testsuite/openshift/objects/envoy/route.py new file mode 100644 index 00000000..260ec461 --- /dev/null +++ b/testsuite/openshift/objects/envoy/route.py @@ -0,0 +1,60 @@ +"""GatewayRoute implementation for pure Envoy Deployment""" +from testsuite.objects.gateway import GatewayRoute, Gateway +from testsuite.openshift.client import OpenShiftClient +from testsuite.openshift.httpbin import Httpbin +from testsuite.openshift.objects.auth_config import AuthConfig + + +class EnvoyVirtualRoute(GatewayRoute): + """Simulated equivalent of HttpRoute for pure Envoy deployments""" + + @property + def reference(self) -> dict[str, str]: + raise AttributeError("Not Supported for Envoy-only deployment") + + @classmethod + def create_instance(cls, openshift: "OpenShiftClient", name, gateway: Gateway, labels: dict[str, str] = None): + return cls(openshift, gateway) + + def __init__(self, openshift, gateway) -> None: + super().__init__() + self.openshift = openshift + self.gateway = gateway + self.auth_configs: list["AuthConfig"] = [] + self.hostnames: list[str] = [] + + def add_backend(self, backend: "Httpbin", prefix="/"): + self.gateway.config.add_backend(backend, prefix) + self.gateway.rollout() + + def remove_all_backend(self): + self.gateway.config.remove_all_backends() + self.gateway.rollout() + + # Hostname manipulation is not supported with Envoy, Envoy accepts all hostnames + def add_hostname(self, hostname: str): + self.hostnames.append(hostname) + for auth_config in self.auth_configs: + auth_config.add_host(hostname) + + def remove_hostname(self, hostname: str): + self.hostnames.remove(hostname) + for auth_config in self.auth_configs: + auth_config.remove_host(hostname) + + def remove_all_hostnames(self): + self.hostnames.clear() + for auth_config in self.auth_configs: + auth_config.remove_all_hosts() + + def add_auth_config(self, auth_config: AuthConfig): + """Adds AuthConfig to this virtual route""" + self.auth_configs.append(auth_config) + for hostname in self.hostnames: + auth_config.add_host(hostname) + + def commit(self): + return + + def delete(self): + return diff --git a/testsuite/openshift/objects/envoy/tls.py b/testsuite/openshift/objects/envoy/tls.py new file mode 100644 index 00000000..7efe1eab --- /dev/null +++ b/testsuite/openshift/objects/envoy/tls.py @@ -0,0 +1,85 @@ +"""Envoy Gateway implementation with TLS setup""" +from importlib import resources +from typing import TYPE_CHECKING + +import yaml + +from . import Envoy, EnvoyConfig + +if TYPE_CHECKING: + from ...client import OpenShiftClient + +TLS_TRANSPORT = """ +name: envoy.transport_sockets.tls +typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + require_client_certificate: true + common_tls_context: + tls_certificates: + - certificate_chain: {filename: "/etc/ssl/certs/envoy/tls.crt"} + private_key: {filename: "/etc/ssl/certs/envoy/tls.key"} + validation_context: + trusted_ca: + filename: "/etc/ssl/certs/envoy-ca/tls.crt" +""" + +UPSTREAM_TLS_TRANSPORT = """ +name: envoy.transport_sockets.tls +typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + validation_context: + trusted_ca: + filename: /etc/ssl/certs/authorino/tls.crt +""" + + +class TLSEnvoy(Envoy): + """Envoy setup with TLS""" + + def __init__( + self, + openshift: "OpenShiftClient", + name, + authorino, + image, + authorino_ca_secret, + envoy_ca_secret, + envoy_cert_secret, + labels: dict[str, str], + ) -> None: + super().__init__(openshift, name, authorino, image, labels) + self.authorino_ca_secret = authorino_ca_secret + self.backend_ca_secret = envoy_ca_secret + self.envoy_cert_secret = envoy_cert_secret + + @property + def config(self): + if not self._config: + self._config = EnvoyConfig.create_instance(self.openshift, self.name, self.authorino, self.labels) + config = yaml.safe_load(self._config["envoy.yaml"]) + config["static_resources"]["listeners"][0]["filter_chains"][0]["transport_socket"] = yaml.safe_load( + TLS_TRANSPORT + ) + for cluster in config["static_resources"]["clusters"]: + if cluster["name"] == "external_auth": + cluster["transport_socket"] = yaml.safe_load(UPSTREAM_TLS_TRANSPORT) + self._config["envoy.yaml"] = yaml.dump(config) + return self._config + + def commit(self): + self.config.commit() + self.envoy_objects = self.openshift.new_app( + resources.files("testsuite.resources.tls").joinpath("envoy.yaml"), + { + "NAME": self.name, + "LABEL": self.app_label, + "AUTHORINO_CA_SECRET": self.authorino_ca_secret, + "ENVOY_CA_SECRET": self.backend_ca_secret, + "ENVOY_CERT_SECRET": self.envoy_cert_secret, + "ENVOY_IMAGE": self.image, + }, + ) + + with self.openshift.context: + assert self.openshift.is_ready(self.envoy_objects.narrow("deployment")), "Envoy wasn't ready in time" diff --git a/testsuite/openshift/objects/envoy/wristband.py b/testsuite/openshift/objects/envoy/wristband.py new file mode 100644 index 00000000..f510b2b0 --- /dev/null +++ b/testsuite/openshift/objects/envoy/wristband.py @@ -0,0 +1,32 @@ +"""Wristband Envoy""" +import yaml + +from testsuite.openshift.objects.envoy import Envoy, EnvoyConfig + + +class WristbandEnvoy(Envoy): + """Envoy configuration with Wristband setup""" + + @property + def config(self): + if not self._config: + self._config = EnvoyConfig.create_instance(self.openshift, self.name, self.authorino, self.labels) + config = yaml.safe_load(self._config["envoy.yaml"]) + config["static_resources"]["listeners"][0]["filter_chains"][0]["filters"][0]["typed_config"][ + "route_config" + ]["virtual_hosts"][0]["routes"].append( + { + "match": {"prefix": "/auth"}, + "directResponse": {"status": 200}, + "response_headers_to_add": [ + { + "header": { + "key": "wristband-token", + "value": '%DYNAMIC_METADATA(["envoy.filters.http.ext_authz", "wristband"])%', + } + } + ], + } + ) + self._config["envoy.yaml"] = yaml.dump(config) + return self._config diff --git a/testsuite/openshift/objects/gateway_api/__init__.py b/testsuite/openshift/objects/gateway_api/__init__.py index a08fe5c5..e69de29b 100644 --- a/testsuite/openshift/objects/gateway_api/__init__.py +++ b/testsuite/openshift/objects/gateway_api/__init__.py @@ -1,37 +0,0 @@ -"""Module containing all Gateway API related classes""" -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Any, Optional - -from testsuite.objects import asdict - - -class Referencable(ABC): - """Object that can be referenced in Gateway API style""" - - @property - @abstractmethod - def reference(self) -> dict[str, str]: - """ - Returns dict, which can be used as reference in Gateway API Objects. - https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference - """ - - -@dataclass -class CustomReference(Referencable): - """ - Manually creates Reference object. - https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.ParentReference - """ - - @property - def reference(self) -> dict[str, Any]: - return asdict(self) - - group: str - kind: str - name: str - namespace: Optional[str] = None - sectionName: Optional[str] = None # pylint: disable=invalid-name - port: Optional[int] = None diff --git a/testsuite/openshift/objects/gateway_api/gateway.py b/testsuite/openshift/objects/gateway_api/gateway.py index 2bc97cd5..ba72797a 100644 --- a/testsuite/openshift/objects/gateway_api/gateway.py +++ b/testsuite/openshift/objects/gateway_api/gateway.py @@ -1,33 +1,21 @@ """Module containing all gateway classes""" +# mypy: disable-error-code="override" import json -import typing +from typing import Optional -from openshift import Selector, timeout, selector +import openshift as oc from testsuite.certificates import Certificate +from testsuite.objects.gateway import Gateway from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.objects import OpenShiftObject -from testsuite.openshift.objects.proxy import Proxy -from testsuite.openshift.objects.route import Route, OpenshiftRoute -from . import Referencable -from .route import HTTPRoute, HostnameWrapper -if typing.TYPE_CHECKING: - from testsuite.openshift.httpbin import Httpbin - -class Gateway(OpenShiftObject, Referencable): +class KuadrantGateway(OpenShiftObject, Gateway): """Gateway object for purposes of MGC""" @classmethod - def create_instance( - cls, - openshift: OpenShiftClient, - name: str, - gateway_class: str, - hostname: str, - labels: dict[str, str] = None, - ): + def create_instance(cls, openshift: OpenShiftClient, name, hostname, labels): """Creates new instance of Gateway""" model = { @@ -35,7 +23,7 @@ def create_instance( "kind": "Gateway", "metadata": {"name": name, "labels": labels}, "spec": { - "gatewayClassName": gateway_class, + "gatewayClassName": "istio", "listeners": [ { "name": "api", @@ -50,19 +38,31 @@ def create_instance( return cls(model, context=openshift.context) - def wait_for_ready(self) -> bool: - """Waits for the gateway to be ready""" - return True + @property + def service_name(self) -> str: + return f"{self.name()}-istio" @property def openshift(self): """Hostname of the first listener""" return OpenShiftClient.from_context(self.context) - @property - def hostname(self): - """Hostname of the first listener""" - return self.model.spec.listeners[0].hostname + def is_ready(self): + """Check the programmed status""" + for condition in self.model.status.conditions: + if condition.type == "Programmed" and condition.status == "True": + return True + return False + + def wait_for_ready(self, timeout: int = 180): + """Waits for the gateway to be ready in the sense of is_ready(self)""" + with oc.timeout(timeout): + success, _, _ = self.self_selector().until_all(success_func=lambda obj: MGCGateway(obj.model).is_ready()) + assert success, "Gateway didn't get ready in time" + self.refresh() + + def get_tls_cert(self): + return None @property def reference(self): @@ -74,7 +74,7 @@ def reference(self): } -class MGCGateway(Gateway): +class MGCGateway(KuadrantGateway): """Gateway object for purposes of MGC""" @classmethod @@ -85,9 +85,9 @@ def create_instance( gateway_class: str, hostname: str, labels: dict[str, str] = None, - tls: bool = False, - placement: typing.Optional[str] = None, - ): + tls: bool = True, + placement: str = None, + ): # pylint: disable=arguments-renamed """Creates new instance of Gateway""" if labels is None: labels = {} @@ -95,10 +95,10 @@ def create_instance( if placement is not None: labels["cluster.open-cluster-management.io/placement"] = placement - instance = super(MGCGateway, cls).create_instance(openshift, name, gateway_class, hostname, labels) - + instance = super(MGCGateway, cls).create_instance(openshift, name, hostname, labels) + instance.model.spec.gatewayClassName = gateway_class if tls: - instance.model["spec"]["listeners"] = [ + instance.model.spec.listeners = [ { "name": "api", "port": 443, @@ -114,8 +114,11 @@ def create_instance( return instance - def get_tls_cert(self) -> Certificate: + def get_tls_cert(self) -> Optional[Certificate]: """Returns TLS certificate used by the gateway""" + if "tls" not in self.model.spec.listeners[0]: + return None + tls_cert_secret_name = self.cert_secret_name tls_cert_secret = self.openshift.get_secret(tls_cert_secret_name) tls_cert = Certificate( @@ -128,7 +131,7 @@ def get_tls_cert(self) -> Certificate: def delete_tls_secret(self): """Deletes secret with TLS certificate used by the gateway""" with self.openshift.context: - selector(f"secret/{self.cert_secret_name}").delete(ignore_not_found=True) + oc.selector(f"secret/{self.cert_secret_name}").delete(ignore_not_found=True) def get_spoke_gateway(self, spokes: dict[str, OpenShiftClient]) -> "MGCGateway": """ @@ -141,72 +144,9 @@ def get_spoke_gateway(self, spokes: dict[str, OpenShiftClient]) -> "MGCGateway": prefix = "kuadrant" spoke_client = spoke_client.change_project(f"{prefix}-{self.namespace()}") with spoke_client.context: - return selector(f"gateway/{self.name()}").object(cls=self.__class__) - - def is_ready(self): - """Check the programmed status""" - for condition in self.model.status.conditions: - if condition.type == "Programmed" and condition.status == "True": - return True - return False - - def wait_for_ready(self): - """Waits for the gateway to be ready in the sense of is_ready(self)""" - with timeout(600): - success, _, _ = self.self_selector().until_all( - success_func=lambda obj: self.__class__(obj.model).is_ready() - ) - assert success, "Gateway didn't get ready in time" - self.refresh() - return success - - def delete(self, ignore_not_found=True, cmd_args=None): - with timeout(90): - super().delete(ignore_not_found, cmd_args) + return oc.selector(f"gateway/{self.name()}").object(cls=self.__class__) @property def cert_secret_name(self): """Returns name of the secret with generated TLS certificate""" return self.model.spec.listeners[0].tls.certificateRefs[0].name - - -class GatewayProxy(Proxy): - """Wrapper for Gateway object to make it a Proxy implementation e.g. exposing hostnames outside of the cluster""" - - def __init__(self, gateway: Gateway, label, backend: "Httpbin") -> None: - super().__init__() - self.openshift = gateway.openshift - self.gateway = gateway - self.name = gateway.name() - self.label = label - self.backend = backend - - self.route: HTTPRoute = None # type: ignore - self.selector: Selector = None # type: ignore - - def expose_hostname(self, name) -> Route: - route = OpenshiftRoute.create_instance(self.openshift, name, f"{self.name}-istio", "api") - route.commit() - if self.route is None: - self.route = HTTPRoute.create_instance( - self.openshift, - self.name, - self.gateway, - route.model.spec.host, - self.backend, - labels={"app": self.label}, - ) - self.selector = self.route.self_selector() - self.route.commit() - else: - self.route.add_hostname(route.model.spec.host) - self.selector = self.selector.union(route.self_selector()) - return HostnameWrapper(self.route, route.model.spec.host) - - def commit(self): - pass - - def delete(self): - if self.selector: - self.selector.delete() - self.selector = None diff --git a/testsuite/openshift/objects/gateway_api/hostname.py b/testsuite/openshift/objects/gateway_api/hostname.py new file mode 100644 index 00000000..d2271923 --- /dev/null +++ b/testsuite/openshift/objects/gateway_api/hostname.py @@ -0,0 +1,71 @@ +"""Module containing implementation for Hostname related classes of Gateway API""" +from httpx import Client + +from testsuite.certificates import Certificate +from testsuite.httpx import KuadrantClient +from testsuite.objects import LifecycleObject +from testsuite.objects.gateway import Gateway +from testsuite.objects.hostname import Exposer, Hostname +from testsuite.openshift.objects.route import OpenshiftRoute + + +class OpenShiftExposer(Exposer, LifecycleObject): + """Exposes hostnames through OpenShift Route objects""" + + def __init__(self, passthrough=False) -> None: + super().__init__() + self.routes: list[OpenshiftRoute] = [] + self.passthrough = passthrough + + def expose_hostname(self, name, gateway: Gateway) -> Hostname: + tls = False + termination = "edge" + if self.passthrough: + tls = True + termination = "passthrough" + route = OpenshiftRoute.create_instance( + gateway.openshift, name, gateway.service_name, "api", tls=tls, termination=termination + ) + self.routes.append(route) + route.commit() + return route + + def commit(self): + return + + def delete(self): + for route in self.routes: + route.delete() + self.routes = [] + + +class StaticHostname(Hostname): + """Already exposed hostname object""" + + def __init__(self, hostname, tls_cert: Certificate = None): + super().__init__() + self._hostname = hostname + self.tls_cert = tls_cert + + def client(self, **kwargs) -> Client: + protocol = "http" + if self.tls_cert: + protocol = "https" + kwargs.setdefault("verify", self.tls_cert) + return KuadrantClient(base_url=f"{protocol}://{self.hostname}", **kwargs) + + @property + def hostname(self): + return self._hostname + + +class DNSPolicyExposer(Exposer): + """Exposing is done as part of DNSPolicy, so no work needs to be done here""" + + def __init__(self, base_domain, tls_cert: Certificate = None): + super().__init__() + self.base_domain = base_domain + self.tls_cert = tls_cert + + def expose_hostname(self, name, gateway: Gateway) -> Hostname: + return StaticHostname(f"{name}.{self.base_domain}", gateway.get_tls_cert()) diff --git a/testsuite/openshift/objects/gateway_api/route.py b/testsuite/openshift/objects/gateway_api/route.py index 4ece3ae7..f5f4660c 100644 --- a/testsuite/openshift/objects/gateway_api/route.py +++ b/testsuite/openshift/objects/gateway_api/route.py @@ -2,23 +2,18 @@ import typing -from functools import cached_property - from httpx import Client from testsuite.httpx import KuadrantClient +from testsuite.objects.gateway import GatewayRoute, Gateway from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.objects import modify, OpenShiftObject -from testsuite.openshift.objects.route import Route - -from . import Referencable - if typing.TYPE_CHECKING: from testsuite.openshift.httpbin import Httpbin -class HTTPRoute(OpenShiftObject, Referencable): +class HTTPRoute(OpenShiftObject, GatewayRoute): """HTTPRoute object, serves as replacement for Routes and Ingresses""" def client(self, **kwargs) -> Client: @@ -28,11 +23,9 @@ def client(self, **kwargs) -> Client: @classmethod def create_instance( cls, - openshift: OpenShiftClient, + openshift: "OpenShiftClient", name, - parent: Referencable, - hostname, - backend: "Httpbin", + gateway: Gateway, labels: dict[str, str] = None, ): """Creates new instance of HTTPRoute""" @@ -41,9 +34,9 @@ def create_instance( "kind": "HTTPRoute", "metadata": {"name": name, "namespace": openshift.project, "labels": labels}, "spec": { - "parentRefs": [parent.reference], - "hostnames": [hostname], - "rules": [{"backendRefs": [backend.reference]}], + "parentRefs": [gateway.reference], + "hostnames": [], + "rules": [], }, } @@ -64,13 +57,13 @@ def hostnames(self): return self.model.spec.hostnames @modify - def add_hostname(self, hostname): + def add_hostname(self, hostname: str): """Adds hostname to the Route""" if hostname not in self.model.spec.hostnames: self.model.spec.hostnames.append(hostname) @modify - def remove_hostname(self, hostname): + def remove_hostname(self, hostname: str): """Adds hostname to the Route""" self.model.spec.hostnames.remove(hostname) @@ -80,38 +73,24 @@ def remove_all_hostnames(self): self.model.spec.hostnames = [] @modify - def set_match(self, path_prefix: str = None, headers: dict[str, str] = None): + 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"} - if headers: - match["headers"] = headers - self.model.spec.rules[0]["matches"] = [match] - - -class HostnameWrapper(Route, Referencable): - """ - Wraps HTTPRoute with Route interface with specific hostname defined for a client - Needed because there can be only HTTPRoute for Kuadrant, while there will be multiple OpenshiftRoutes for AuthConfig - """ - - def __init__(self, route: HTTPRoute, hostname: str) -> None: - super().__init__() - self.route = route - self._hostname = hostname + 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") - @cached_property - def hostname(self) -> str: - return self._hostname - - def client(self, **kwargs) -> Client: - return KuadrantClient(base_url=f"http://{self.hostname}", **kwargs) - - @property - def reference(self) -> dict[str, str]: - return self.route.reference + @modify + def add_backend(self, backend: "Httpbin", prefix="/"): + self.model.spec.rules.append( + {"backendRefs": [backend.reference], "matches": [{"path": {"value": prefix, "type": "PathPrefix"}}]} + ) - def __getattr__(self, attr): - """Direct all other calls to the original route""" - return getattr(self.route, attr) + @modify + def remove_all_backend(self): + self.model.spec.rules.clear() diff --git a/testsuite/openshift/objects/proxy.py b/testsuite/openshift/objects/proxy.py deleted file mode 100644 index ad58a85b..00000000 --- a/testsuite/openshift/objects/proxy.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Module containing Proxy related stuff""" -from abc import abstractmethod - -from testsuite.objects import LifecycleObject -from testsuite.openshift.objects.route import Route - - -class Proxy(LifecycleObject): - """Abstraction layer for a Proxy sitting between end-user and Kuadrant""" - - @abstractmethod - def expose_hostname(self, name) -> Route: - """Exposes hostname to point to this Proxy""" diff --git a/testsuite/openshift/objects/rate_limit.py b/testsuite/openshift/objects/rate_limit.py index 81c43972..959adfeb 100644 --- a/testsuite/openshift/objects/rate_limit.py +++ b/testsuite/openshift/objects/rate_limit.py @@ -6,9 +6,9 @@ import openshift as oc from testsuite.objects import Rule, asdict +from testsuite.objects.gateway import Referencable from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.objects import OpenShiftObject, modify -from testsuite.openshift.objects.gateway_api import Referencable @dataclass diff --git a/testsuite/openshift/objects/route.py b/testsuite/openshift/objects/route.py index fbb3090d..5bd60874 100644 --- a/testsuite/openshift/objects/route.py +++ b/testsuite/openshift/objects/route.py @@ -1,27 +1,14 @@ """Module containing Route related stuff""" -from abc import ABC, abstractmethod from functools import cached_property from httpx import Client from testsuite.httpx import KuadrantClient +from testsuite.objects.hostname import Hostname from testsuite.openshift.objects import OpenShiftObject -class Route(ABC): - """Abstraction layer for Route/Ingress/HTTPRoute""" - - @cached_property - @abstractmethod - def hostname(self) -> str: - """Returns one of the Route valid hostnames""" - - @abstractmethod - def client(self, **kwargs) -> Client: - """Return Httpx client for the requests to this backend""" - - -class OpenshiftRoute(OpenShiftObject, Route): +class OpenshiftRoute(OpenShiftObject, Hostname): """Openshift Route object""" @classmethod diff --git a/testsuite/openshift/objects/tlspolicy.py b/testsuite/openshift/objects/tlspolicy.py index 33728270..f32cfbcf 100644 --- a/testsuite/openshift/objects/tlspolicy.py +++ b/testsuite/openshift/objects/tlspolicy.py @@ -1,7 +1,7 @@ """Module for TLSPolicy related classes""" +from testsuite.objects.gateway import Referencable from testsuite.openshift.client import OpenShiftClient from testsuite.openshift.objects import OpenShiftObject -from testsuite.openshift.objects.gateway_api import Referencable class TLSPolicy(OpenShiftObject): diff --git a/testsuite/resources/envoy.yaml b/testsuite/resources/envoy.yaml index ad0e5b7d..8f2d3664 100644 --- a/testsuite/resources/envoy.yaml +++ b/testsuite/resources/envoy.yaml @@ -3,96 +3,6 @@ kind: Template metadata: name: envoy-template objects: -- apiVersion: v1 - kind: ConfigMap - metadata: - labels: - app: ${LABEL} - name: ${NAME} - data: - envoy.yaml: | - static_resources: - listeners: - - address: - socket_address: - address: 0.0.0.0 - port_value: 8000 - filter_chains: - - filters: - - name: envoy.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: local - route_config: - name: local_route - virtual_hosts: - - name: local_service - domains: ['*'] - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - check_settings: - context_extensions: - virtual_host: local_service - routes: - - match: { prefix: / } - route: - cluster: httpbin - http_filters: - - name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - failure_mode_allow: false - status_on_error: {code: 500} - include_peer_certificate: true - grpc_service: - envoy_grpc: - cluster_name: external_auth - timeout: 1s - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - use_remote_address: true - clusters: - - name: external_auth - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: external_auth - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ${AUTHORINO_URL} - port_value: 50051 - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - upstream_http_protocol_options: - auto_sni: true - explicit_http_config: - http2_protocol_options: {} - - name: httpbin - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: httpbin - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ${UPSTREAM_URL} - port_value: 8080 - admin: - address: - socket_address: - address: 0.0.0.0 - port_value: 8001 - apiVersion: apps/v1 kind: Deployment metadata: @@ -122,6 +32,12 @@ objects: - /usr/local/bin/envoy image: ${ENVOY_IMAGE} name: envoy + readinessProbe: + httpGet: + path: /ready + port: 8001 + initialDelaySeconds: 3 + periodSeconds: 4 ports: - containerPort: 8000 name: web @@ -146,7 +62,7 @@ objects: name: ${NAME} spec: ports: - - name: web + - name: api port: 8000 protocol: TCP selector: @@ -159,11 +75,5 @@ parameters: - name: LABEL description: "App label for all resources" required: true -- name: AUTHORINO_URL - description: "Authorino URL" - required: true -- name: UPSTREAM_URL - description: "URL for the upstream/backend" - required: true - name: ENVOY_IMAGE - required: false + required: true diff --git a/testsuite/resources/tls/envoy.yaml b/testsuite/resources/tls/envoy.yaml index d2daf0e2..53edd62f 100644 --- a/testsuite/resources/tls/envoy.yaml +++ b/testsuite/resources/tls/envoy.yaml @@ -3,116 +3,6 @@ kind: Template metadata: name: envoy-tls-template objects: -- apiVersion: v1 - kind: ConfigMap - metadata: - labels: - app: ${LABEL} - name: ${NAME} - data: - envoy.yaml: | - static_resources: - listeners: - - address: - socket_address: - address: 0.0.0.0 - port_value: 8000 - filter_chains: - - transport_socket: - name: envoy.transport_sockets.tls - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext - require_client_certificate: true - common_tls_context: - tls_certificates: - - certificate_chain: {filename: "/etc/ssl/certs/envoy/tls.crt"} - private_key: {filename: "/etc/ssl/certs/envoy/tls.key"} - validation_context: - trusted_ca: - filename: "/etc/ssl/certs/envoy-ca/tls.crt" - filters: - - name: envoy.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: local - route_config: - name: local_route - virtual_hosts: - - name: local_service - domains: ['*'] - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - check_settings: - context_extensions: - virtual_host: local_service - routes: - - match: { prefix: / } - route: - cluster: httpbin - http_filters: - - name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - failure_mode_allow: false - status_on_error: {code: 500} - include_peer_certificate: true - grpc_service: - envoy_grpc: - cluster_name: external_auth - timeout: 1s - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - use_remote_address: true - clusters: - - name: external_auth - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: external_auth - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ${AUTHORINO_URL} - port_value: 50051 - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - upstream_http_protocol_options: - auto_sni: true - explicit_http_config: - http2_protocol_options: {} - transport_socket: - name: envoy.transport_sockets.tls - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext - common_tls_context: - validation_context: - trusted_ca: - filename: /etc/ssl/certs/authorino/tls.crt - - name: httpbin - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: httpbin - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ${UPSTREAM_URL} - port_value: 8080 - admin: - address: - socket_address: - address: 0.0.0.0 - port_value: 8001 - apiVersion: apps/v1 kind: Deployment metadata: @@ -141,6 +31,12 @@ objects: command: - /usr/local/bin/envoy image: ${ENVOY_IMAGE} + readinessProbe: + httpGet: + path: /ready + port: 8001 + initialDelaySeconds: 3 + periodSeconds: 4 name: envoy ports: - containerPort: 8000 @@ -184,7 +80,7 @@ objects: name: ${NAME} spec: ports: - - name: web + - name: api port: 8000 protocol: TCP selector: @@ -197,12 +93,6 @@ parameters: - name: LABEL description: "App label for all resources" required: true -- name: AUTHORINO_URL - description: "Authorino URL" - required: true -- name: UPSTREAM_URL - description: "URL for the upstream/backend" - required: true - name: ENVOY_CERT_SECRET description: "Secret containing certificate for envoy" required: true diff --git a/testsuite/resources/wristband/__init__.py b/testsuite/resources/wristband/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/testsuite/resources/wristband/envoy.yaml b/testsuite/resources/wristband/envoy.yaml deleted file mode 100644 index 3e0923be..00000000 --- a/testsuite/resources/wristband/envoy.yaml +++ /dev/null @@ -1,176 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -metadata: - name: envoy-template -objects: -- apiVersion: v1 - kind: ConfigMap - metadata: - labels: - app: ${LABEL} - name: ${NAME} - data: - envoy.yaml: | - static_resources: - listeners: - - address: - socket_address: - address: 0.0.0.0 - port_value: 8000 - filter_chains: - - filters: - - name: envoy.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: local - route_config: - name: local_route - virtual_hosts: - - name: local_service - domains: ['*'] - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - check_settings: - context_extensions: - virtual_host: local_service - routes: - - match: { prefix: /auth } - direct_response: - status: 200 - response_headers_to_add: - - header: - key: wristband-token - value: '%DYNAMIC_METADATA(["envoy.filters.http.ext_authz", "wristband"])%' - - match: { prefix: / } - route: - cluster: httpbin - http_filters: - - name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - failure_mode_allow: false - status_on_error: {code: 500} - include_peer_certificate: true - grpc_service: - envoy_grpc: - cluster_name: external_auth - timeout: 1s - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - use_remote_address: true - clusters: - - name: external_auth - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: external_auth - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ${AUTHORINO_URL} - port_value: 50051 - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - upstream_http_protocol_options: - auto_sni: true - explicit_http_config: - http2_protocol_options: {} - - name: httpbin - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - load_assignment: - cluster_name: httpbin - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ${UPSTREAM_URL} - port_value: 8080 - admin: - address: - socket_address: - address: 0.0.0.0 - port_value: 8001 -- apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - app: ${LABEL} - svc: envoy - name: ${NAME} - spec: - replicas: 1 - selector: - matchLabels: - app: ${LABEL} - svc: envoy - template: - metadata: - labels: - app: ${LABEL} - svc: envoy - spec: - containers: - - args: - - --config-path /usr/local/etc/envoy/envoy.yaml - - --service-cluster front-proxy - - --log-level info - - --component-log-level filter:trace,http:debug,router:debug - command: - - /usr/local/bin/envoy - image: ${ENVOY_IMAGE} - name: envoy - ports: - - containerPort: 8000 - name: web - - containerPort: 8001 - name: admin - volumeMounts: - - mountPath: /usr/local/etc/envoy - name: config - readOnly: true - volumes: - - configMap: - items: - - key: envoy.yaml - path: envoy.yaml - name: ${NAME} - name: config -- apiVersion: v1 - kind: Service - metadata: - labels: - app: ${LABEL} - name: ${NAME} - spec: - ports: - - name: web - port: 8000 - protocol: TCP - selector: - app: ${LABEL} - svc: envoy -parameters: -- name: NAME - description: "Name for the resources created" - required: true -- name: LABEL - description: "App label for all resources" - required: true -- name: AUTHORINO_URL - description: "Authorino URL" - required: true -- name: UPSTREAM_URL - description: "URL for the upstream/backend" - required: true -- name: ENVOY_IMAGE - required: false diff --git a/testsuite/tests/conftest.py b/testsuite/tests/conftest.py index 7ead8c3f..1c2d5b16 100644 --- a/testsuite/tests/conftest.py +++ b/testsuite/tests/conftest.py @@ -9,14 +9,17 @@ from testsuite.certificates import CFSSLClient from testsuite.config import settings from testsuite.mockserver import Mockserver +from testsuite.objects.gateway import Gateway, GatewayRoute +from testsuite.objects.hostname import Exposer, Hostname from testsuite.oidc import OIDCProvider from testsuite.oidc.auth0 import Auth0Provider -from testsuite.oidc.rhsso import RHSSO -from testsuite.openshift.envoy import Envoy from testsuite.openshift.httpbin import Httpbin -from testsuite.openshift.objects.gateway_api.gateway import GatewayProxy, Gateway -from testsuite.openshift.objects.proxy import Proxy -from testsuite.openshift.objects.route import Route +from testsuite.oidc.rhsso import RHSSO +from testsuite.openshift.objects.envoy import Envoy +from testsuite.openshift.objects.envoy.route import EnvoyVirtualRoute +from testsuite.openshift.objects.gateway_api.gateway import KuadrantGateway +from testsuite.openshift.objects.gateway_api.hostname import OpenShiftExposer +from testsuite.openshift.objects.gateway_api.route import HTTPRoute from testsuite.utils import randomize, _whoami @@ -240,39 +243,52 @@ def backend(request, openshift, blame, label): @pytest.fixture(scope="module") -def gateway(request, openshift, blame, wildcard_domain, module_label) -> Gateway: - """Gateway object to use when working with Gateway API""" - gateway = Gateway.create_instance(openshift, blame("gw"), "istio", wildcard_domain, {"app": module_label}) - request.addfinalizer(gateway.delete) - gateway.commit() - gateway.wait_for_ready() - return gateway - - -@pytest.fixture(scope="module") -def proxy(request, kuadrant, authorino, openshift, blame, backend, module_label, testconfig) -> Proxy: +def gateway(request, kuadrant, openshift, blame, backend, module_label, testconfig, wildcard_domain) -> Gateway: """Deploys Envoy that wire up the Backend behind the reverse-proxy and Authorino instance""" if kuadrant: - gateway_object = request.getfixturevalue("gateway") - envoy: Proxy = GatewayProxy(gateway_object, module_label, backend) + gw = KuadrantGateway.create_instance(openshift, blame("gw"), wildcard_domain, {"app": module_label}) else: - envoy = Envoy( + authorino = request.getfixturevalue("authorino") + gw = Envoy( openshift, + blame("gw"), authorino, - blame("envoy"), - module_label, - backend, testconfig["service_protection"]["envoy"]["image"], + labels={"app": module_label}, ) - request.addfinalizer(envoy.delete) - envoy.commit() - return envoy + request.addfinalizer(gw.delete) + gw.commit() + return gw + + +@pytest.fixture(scope="module") +def route(request, kuadrant, gateway, blame, hostname, backend, module_label) -> GatewayRoute: + """Route object""" + if kuadrant: + route = HTTPRoute.create_instance(gateway.openshift, blame("route"), gateway, {"app": module_label}) + else: + route = EnvoyVirtualRoute.create_instance(gateway.openshift, blame("route"), gateway) + route.add_hostname(hostname.hostname) + route.add_backend(backend) + request.addfinalizer(route.delete) + route.commit() + return route + + +@pytest.fixture(scope="module") +def exposer(request) -> Exposer: + """Exposer object instance""" + exposer = OpenShiftExposer() + request.addfinalizer(exposer.delete) + exposer.commit() + return exposer @pytest.fixture(scope="module") -def route(proxy, module_label) -> Route: - """Exposed Route object""" - return proxy.expose_hostname(module_label) +def hostname(gateway, exposer, blame) -> Hostname: + """Exposed Hostname object""" + hostname = exposer.expose_hostname(blame("hostname"), gateway) + return hostname @pytest.fixture(scope="module") @@ -281,3 +297,11 @@ def wildcard_domain(openshift): Wildcard domain of openshift cluster """ return f"*.{openshift.apps_url}" + + +@pytest.fixture(scope="module") +def client(route, hostname): + """Returns httpx client to be used for requests, it also commits AuthConfig""" + client = hostname.client() + yield client + client.close() diff --git a/testsuite/tests/kuadrant/authorino/dinosaur/conftest.py b/testsuite/tests/kuadrant/authorino/dinosaur/conftest.py index 4f4e0e36..95b318c8 100644 --- a/testsuite/tests/kuadrant/authorino/dinosaur/conftest.py +++ b/testsuite/tests/kuadrant/authorino/dinosaur/conftest.py @@ -7,6 +7,7 @@ from testsuite.httpx.auth import HttpxOidcClientAuth from testsuite.oidc.rhsso import RHSSO +from testsuite.openshift.objects.auth_config import AuthConfig from testsuite.utils import ContentType @@ -95,7 +96,7 @@ def commit(authorization): def authorization( openshift, blame, - route, + hostname, module_label, rhsso, terms_and_conditions, @@ -103,15 +104,19 @@ def authorization( admin_rhsso, resource_info, request, + route, ): """Creates AuthConfig object from template""" - auth = openshift.new_app( + # with openshift.context: + # name = blame("ac") + # request.addfinalizer(lambda: selector(f"authconfig/{name}").delete(ignore_not_found=True)) + selector = openshift.new_app( resources.files("testsuite.resources").joinpath("dinosaur_config.yaml"), { "NAME": blame("ac"), "NAMESPACE": openshift.project, "LABEL": module_label, - "HOST": route.hostname, + "HOST": hostname.hostname, "RHSSO_ISSUER": rhsso.well_known["issuer"], "ADMIN_ISSUER": admin_rhsso.well_known["issuer"], "TERMS_AND_CONDITIONS": terms_and_conditions("false"), @@ -119,8 +124,10 @@ def authorization( "RESOURCE_INFO": resource_info("123", rhsso.client_name), }, ) - - request.addfinalizer(auth.delete) + with openshift.context: + auth = selector.object(cls=AuthConfig) + request.addfinalizer(auth.delete) + route.add_auth_config(auth) return auth diff --git a/testsuite/tests/kuadrant/authorino/identity/api_key/test_auth_credentials.py b/testsuite/tests/kuadrant/authorino/identity/api_key/test_auth_credentials.py index 49f63677..a973d6de 100644 --- a/testsuite/tests/kuadrant/authorino/identity/api_key/test_auth_credentials.py +++ b/testsuite/tests/kuadrant/authorino/identity/api_key/test_auth_credentials.py @@ -47,9 +47,9 @@ def test_query(client, auth, credentials): assert response.status_code == 401 -def test_cookie(route, auth, credentials): +def test_cookie(hostname, auth, credentials): """Test if auth credentials are stored in right place""" - with route.client(cookies={"APIKEY": auth.api_key}) as client: + with hostname.client(cookies={"APIKEY": auth.api_key}) as client: response = client.get("/get") if credentials == "cookie": assert response.status_code == 200 diff --git a/testsuite/tests/kuadrant/authorino/identity/rhsso/test_auth_credentials.py b/testsuite/tests/kuadrant/authorino/identity/rhsso/test_auth_credentials.py index 11de3dd1..45b5a386 100644 --- a/testsuite/tests/kuadrant/authorino/identity/rhsso/test_auth_credentials.py +++ b/testsuite/tests/kuadrant/authorino/identity/rhsso/test_auth_credentials.py @@ -45,9 +45,9 @@ def test_query(client, auth, credentials): assert response.status_code == 401 -def test_cookie(route, auth, credentials): +def test_cookie(hostname, auth, credentials): """Test if auth credentials are stored in right place""" - with route.client(cookies={"Token": auth.token.access_token}) as client: + with hostname.client(cookies={"Token": auth.token.access_token}) as client: response = client.get("/get") if credentials == "cookie": assert response.status_code == 200 diff --git a/testsuite/tests/kuadrant/authorino/multiple_hosts/conftest.py b/testsuite/tests/kuadrant/authorino/multiple_hosts/conftest.py index d9d9c20b..be03a26a 100644 --- a/testsuite/tests/kuadrant/authorino/multiple_hosts/conftest.py +++ b/testsuite/tests/kuadrant/authorino/multiple_hosts/conftest.py @@ -3,21 +3,21 @@ @pytest.fixture(scope="module") -def second_route(proxy, blame): - """Second valid hostname""" - return proxy.expose_hostname(blame("second")) +def second_hostname(exposer, gateway, blame): + """Second exposed hostname""" + return exposer.expose_hostname(blame("second"), gateway) @pytest.fixture(scope="module") -def authorization(authorization, second_route): - """Adds second host to the AuthConfig""" - authorization.add_host(second_route.hostname) - return authorization +def route(route, second_hostname): + """Adds second host to the HTTPRoute""" + route.add_hostname(second_hostname.hostname) + return route @pytest.fixture(scope="module") -def client2(second_route): +def client2(second_hostname): """Client for second hostname""" - client = second_route.client() + client = second_hostname.client() yield client client.close() diff --git a/testsuite/tests/kuadrant/authorino/multiple_hosts/test_remove_host.py b/testsuite/tests/kuadrant/authorino/multiple_hosts/test_remove_host.py index b220f278..27ccdb74 100644 --- a/testsuite/tests/kuadrant/authorino/multiple_hosts/test_remove_host.py +++ b/testsuite/tests/kuadrant/authorino/multiple_hosts/test_remove_host.py @@ -1,7 +1,7 @@ """Test host removal""" -def test_removing_host(client, client2, auth, authorization, second_route): +def test_removing_host(client, client2, auth, route, second_hostname): """Tests that after removal of the second host, it stops working, while the first one still works""" response = client.get("/get", auth=auth) assert response.status_code == 200 @@ -9,7 +9,7 @@ def test_removing_host(client, client2, auth, authorization, second_route): response = client2.get("/get", auth=auth) assert response.status_code == 200 - authorization.remove_host(second_route.hostname) + route.remove_hostname(second_hostname.hostname) response = client.get("/get", auth=auth) assert response.status_code == 200 diff --git a/testsuite/tests/kuadrant/authorino/operator/clusterwide/conftest.py b/testsuite/tests/kuadrant/authorino/operator/clusterwide/conftest.py index e5976558..5a96b270 100644 --- a/testsuite/tests/kuadrant/authorino/operator/clusterwide/conftest.py +++ b/testsuite/tests/kuadrant/authorino/operator/clusterwide/conftest.py @@ -11,23 +11,24 @@ def authorino_parameters(): @pytest.fixture(scope="module") -def route2(proxy, blame): +def hostname2(exposer, gateway, blame): """Second route for the envoy""" - return proxy.expose_hostname(blame("route")) + return exposer.expose_hostname(blame("hostname"), gateway) @pytest.fixture(scope="module") -def authorization2(route2, blame, openshift2, module_label, oidc_provider): +def authorization2(route, hostname2, blame, openshift2, module_label, oidc_provider): """Second valid hostname""" - auth = AuthConfig.create_instance(openshift2, blame("ac"), route2, labels={"testRun": module_label}) + route.add_hostname(hostname2.hostname) + auth = AuthConfig.create_instance(openshift2, blame("ac"), route, labels={"testRun": module_label}) auth.identity.add_oidc("rhsso", oidc_provider.well_known["issuer"]) return auth @pytest.fixture(scope="module") -def client2(route2): +def client2(hostname2): """Client for second AuthConfig""" - client = route2.client() + client = hostname2.client() yield client client.close() diff --git a/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py b/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py index f4c669ff..a7c2936e 100644 --- a/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py +++ b/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py @@ -8,24 +8,28 @@ from testsuite.openshift.objects.auth_config import AuthConfig +@pytest.fixture(scope="module") +def route(route, wildcard_domain, hostname): + """Set route for wildcard domain""" + route.add_hostname(wildcard_domain) + route.remove_hostname(hostname.hostname) + return route + + # pylint: disable = unused-argument @pytest.fixture(scope="module") -def authorization(authorino, blame, openshift, module_label, proxy, wildcard_domain): +def authorization(authorino, blame, wildcard_domain, route, openshift, module_label, gateway): """In case of Authorino, AuthConfig used for authorization""" - auth = AuthConfig.create_instance( - openshift, blame("ac"), None, hostnames=[wildcard_domain], labels={"testRun": module_label} - ) + auth = AuthConfig.create_instance(openshift, blame("ac"), route, labels={"testRun": module_label}) auth.responses.add_success_header("header", JsonResponse({"anything": Value("one")})) return auth # pylint: disable = unused-argument @pytest.fixture(scope="module") -def authorization2(authorino, blame, openshift2, module_label, proxy, wildcard_domain): +def authorization2(authorino, blame, route, openshift2, module_label, gateway): """In case of Authorino, AuthConfig used for authorization""" - auth = AuthConfig.create_instance( - openshift2, blame("ac"), None, hostnames=[wildcard_domain], labels={"testRun": module_label} - ) + auth = AuthConfig.create_instance(openshift2, blame("ac"), route, labels={"testRun": module_label}) auth.responses.add_success_header("header", JsonResponse({"anything": Value("two")})) return auth diff --git a/testsuite/tests/kuadrant/authorino/operator/http/conftest.py b/testsuite/tests/kuadrant/authorino/operator/http/conftest.py index 91f303b8..7c3efa6e 100644 --- a/testsuite/tests/kuadrant/authorino/operator/http/conftest.py +++ b/testsuite/tests/kuadrant/authorino/operator/http/conftest.py @@ -9,10 +9,10 @@ # pylint: disable=unused-argument @pytest.fixture(scope="module") -def authorization(authorization, wildcard_domain, openshift, module_label) -> AuthConfig: +def authorization(authorization, route, wildcard_domain, openshift, module_label) -> AuthConfig: """In case of Authorino, AuthConfig used for authorization""" authorization.remove_all_hosts() - authorization.add_host(wildcard_domain) + route.add_hostname(wildcard_domain) authorization.responses.add_success_header("x-ext-auth-other-json", JsonResponse({"propX": Value("valueX")})) return authorization diff --git a/testsuite/tests/kuadrant/authorino/operator/http/test_raw_http.py b/testsuite/tests/kuadrant/authorino/operator/http/test_raw_http.py index 1186206a..230f1c21 100644 --- a/testsuite/tests/kuadrant/authorino/operator/http/test_raw_http.py +++ b/testsuite/tests/kuadrant/authorino/operator/http/test_raw_http.py @@ -5,7 +5,7 @@ # pylint: disable=unused-argument def test_authorized_via_http(authorization, client, auth): - """Test raw http authentization with Keycloak.""" + """Test raw http authentication with Keycloak.""" response = client.get("/check", auth=auth) assert response.status_code == 200 assert response.text == "" @@ -14,7 +14,7 @@ def test_authorized_via_http(authorization, client, auth): # pylint: disable=unused-argument def test_unauthorized_via_http(authorization, client): - """Test raw http authentization with unauthorized request.""" + """Test raw http authentication with unauthorized request.""" response = client.get("/check") assert response.status_code == 401 assert response.text == "" diff --git a/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py b/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py index 04f9b51f..846fce74 100644 --- a/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py +++ b/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py @@ -2,36 +2,57 @@ import pytest from testsuite.objects import Value, JsonResponse -from testsuite.openshift.envoy import Envoy +from testsuite.openshift.objects.envoy import Envoy from testsuite.openshift.objects.auth_config import AuthConfig +from testsuite.openshift.objects.envoy.route import EnvoyVirtualRoute @pytest.fixture(scope="module") -def envoy(request, authorino, openshift, blame, backend, testconfig): - """Envoy""" - - def _envoy(auth=authorino): - name = blame("envoy") - envoy = Envoy(openshift, auth, name, blame("label"), backend, testconfig["envoy"]["image"]) - request.addfinalizer(envoy.delete) - envoy.commit() - route = envoy.expose_hostname(name) - return route +def setup_gateway(request, openshift, blame, testconfig, module_label): + """Factory method for creating Gateways in the test run""" + + def _envoy(auth): + gw = Envoy( + openshift, + blame("gw"), + auth, + testconfig["service_protection"]["envoy"]["image"], + labels={"app": module_label}, + ) + request.addfinalizer(gw.delete) + gw.commit() + return gw return _envoy +@pytest.fixture(scope="module") +def setup_route(request, blame, backend, module_label): + """Factory method for creating Routes in the test run""" + + def _route(hostname, gateway): + route = EnvoyVirtualRoute.create_instance( + gateway.openshift, blame("route"), gateway, labels={"app": module_label} + ) + route.add_hostname(hostname) + route.add_backend(backend) + request.addfinalizer(route.delete) + route.commit() + return route + + return _route + + # pylint: disable=unused-argument @pytest.fixture(scope="module") -def authorization(request, authorino, blame, openshift, module_label): - """In case of Authorino, AuthConfig used for authorization""" +def setup_authorization(request, blame, openshift, module_label): + """Factory method for creating AuthConfigs in the test run""" - def _authorization(hostname=None, sharding_label=None): + def _authorization(route, sharding_label=None): auth = AuthConfig.create_instance( openshift, blame("ac"), - None, - hostnames=[hostname], + route, labels={"testRun": module_label, "sharding": sharding_label}, ) auth.responses.add_success_header("header", JsonResponse({"anything": Value(sharding_label)})) diff --git a/testsuite/tests/kuadrant/authorino/operator/sharding/test_preexisting_auth.py b/testsuite/tests/kuadrant/authorino/operator/sharding/test_preexisting_auth.py index 277fbce2..aa647a7d 100644 --- a/testsuite/tests/kuadrant/authorino/operator/sharding/test_preexisting_auth.py +++ b/testsuite/tests/kuadrant/authorino/operator/sharding/test_preexisting_auth.py @@ -7,18 +7,17 @@ @pytest.fixture(scope="module") -def authorino(openshift, blame, testconfig, module_label, request): +def setup_authorino(openshift, blame, testconfig, module_label, request): """Authorino instance""" def _authorino(sharding_label): - authorino_parameters = {"label_selectors": [f"sharding={sharding_label}", f"testRun={module_label}"]} authorino = AuthorinoCR.create_instance( openshift, blame("authorino"), image=weakget(testconfig)["authorino"]["image"] % None, - **authorino_parameters, + label_selectors=[f"sharding={sharding_label}", f"testRun={module_label}"], ) - request.addfinalizer(lambda: authorino.delete(ignore_not_found=True)) + request.addfinalizer(authorino.delete) authorino.commit() authorino.wait_for_ready() return authorino @@ -26,10 +25,12 @@ def _authorino(sharding_label): return _authorino -@pytest.fixture(scope="module") -def setup(authorino, authorization, envoy, wildcard_domain): +@pytest.mark.issue("https://github.com/Kuadrant/authorino/pull/349") +def test_preexisting_auth( + setup_authorino, setup_authorization, setup_gateway, setup_route, exposer, wildcard_domain, blame +): # pylint: disable=too-many-locals """ - Setup: + Test: - Create AuthConfig A with wildcard - Create Authorino A which will reconcile A - Create Envoy for Authorino A @@ -37,30 +38,25 @@ def setup(authorino, authorization, envoy, wildcard_domain): - Create another Authorino B, which should not reconcile A - Create Envoy for Authorino B - Create AuthConfig B which will have specific host colliding with host A - """ - authorization(wildcard_domain, "A") - custom_authorino = authorino(sharding_label="A") - envoy(custom_authorino) - - custom_authorino.delete() - - custom_authorino2 = authorino(sharding_label="B") - custom_envoy = envoy(custom_authorino2) - auth = authorization(custom_envoy.hostname, "B") - - return custom_envoy, auth - - -@pytest.mark.issue("https://github.com/Kuadrant/authorino/pull/349") -def test_preexisting_auth(setup): - """ - Test: - Assert that AuthConfig B has the host ready and is completely reconciled - Make request to second envoy - Assert that request was processed by right authorino and AuthConfig """ - envoy, auth = setup - assert envoy.hostname in auth.model.status.summary.hostsReady - response = envoy.client().get("/get") + authorino = setup_authorino(sharding_label="A") + gw = setup_gateway(authorino) + route = setup_route(wildcard_domain, gw) + setup_authorization(route, "A") + + authorino.delete() + gw.delete() + + authorino2 = setup_authorino(sharding_label="B") + gw2 = setup_gateway(authorino2) + hostname = exposer.expose_hostname(blame("hostname"), gw2) + route2 = setup_route(hostname.hostname, gw2) + auth = setup_authorization(route2, "B") + + assert hostname.hostname in auth.model.status.summary.hostsReady + response = hostname.client().get("/get") assert response.status_code == 200 assert response.json()["headers"]["Header"] == '{"anything":"B"}' diff --git a/testsuite/tests/kuadrant/authorino/operator/sharding/test_sharding.py b/testsuite/tests/kuadrant/authorino/operator/sharding/test_sharding.py index d2ad22ee..c92a39a9 100644 --- a/testsuite/tests/kuadrant/authorino/operator/sharding/test_sharding.py +++ b/testsuite/tests/kuadrant/authorino/operator/sharding/test_sharding.py @@ -9,7 +9,7 @@ def authorino_parameters(authorino_parameters): yield authorino_parameters -def test_sharding(authorization, authorino, envoy): +def test_sharding(setup_authorization, setup_gateway, setup_route, authorino, exposer, blame): """ Setup: - Create Authorino that watch only AuthConfigs with label `sharding=A` @@ -21,13 +21,18 @@ def test_sharding(authorization, authorino, envoy): - Send a request to the second AuthConfig - Assert that the response status code is 404 """ - envoy1 = envoy(authorino) - envoy2 = envoy(authorino) - authorization(envoy1.hostname, "A") - authorization(envoy2.hostname, "B") + gw = setup_gateway(authorino) + hostname = exposer.expose_hostname(blame("first"), gw) + route = setup_route(hostname.hostname, gw) + setup_authorization(route, sharding_label="A") - response = envoy1.client().get("/get") + gw2 = setup_gateway(authorino) + hostname2 = exposer.expose_hostname(blame("second"), gw2) + route2 = setup_route(hostname2.hostname, gw2) + setup_authorization(route2, sharding_label="B") + + response = hostname.client().get("/get") assert response.status_code == 200 - response = envoy2.client().get("/get") + response = hostname2.client().get("/get") assert response.status_code == 404 diff --git a/testsuite/tests/kuadrant/authorino/operator/test_wildcard.py b/testsuite/tests/kuadrant/authorino/operator/test_wildcard.py index e98ced90..1ae73b4b 100644 --- a/testsuite/tests/kuadrant/authorino/operator/test_wildcard.py +++ b/testsuite/tests/kuadrant/authorino/operator/test_wildcard.py @@ -6,13 +6,18 @@ from testsuite.openshift.objects.auth_config import AuthConfig -# pylint: disable = unused-argument @pytest.fixture(scope="module") -def authorization(authorino, blame, openshift, module_label, wildcard_domain): +def route(route, wildcard_domain, hostname): + """Set route for wildcard domain""" + route.add_hostname(wildcard_domain) + route.remove_hostname(hostname.hostname) + return route + + +@pytest.fixture(scope="module") +def authorization(blame, route, openshift, module_label): """In case of Authorino, AuthConfig used for authorization""" - return AuthConfig.create_instance( - openshift, blame("ac"), None, hostnames=[wildcard_domain], labels={"testRun": module_label} - ) + return AuthConfig.create_instance(openshift, blame("ac"), route, labels={"testRun": module_label}) def test_wildcard(client): diff --git a/testsuite/tests/kuadrant/authorino/operator/tls/conftest.py b/testsuite/tests/kuadrant/authorino/operator/tls/conftest.py index df4f2b90..965f256e 100644 --- a/testsuite/tests/kuadrant/authorino/operator/tls/conftest.py +++ b/testsuite/tests/kuadrant/authorino/operator/tls/conftest.py @@ -5,7 +5,9 @@ from testsuite.certificates import Certificate, CertInfo from testsuite.objects import Selector -from testsuite.openshift.envoy import TLSEnvoy +from testsuite.objects.hostname import Exposer +from testsuite.openshift.objects.envoy.tls import TLSEnvoy +from testsuite.openshift.objects.gateway_api.hostname import OpenShiftExposer from testsuite.openshift.objects.secret import TLSSecret from testsuite.utils import cert_builder @@ -144,14 +146,13 @@ def authorino_labels(selector) -> Dict[str, str]: # pylint: disable-msg=too-many-locals @pytest.fixture(scope="module") -def proxy( +def gateway( request, authorino, openshift, create_secret, blame, - label, - backend, + module_label, authorino_authority, envoy_authority, envoy_cert, @@ -165,20 +166,28 @@ def proxy( envoy = TLSEnvoy( openshift, + blame("gw"), authorino, - blame("backend"), - label, - backend, - testconfig["envoy"]["image"], + testconfig["service_protection"]["envoy"]["image"], authorino_secret, envoy_ca_secret, envoy_secret, + labels={"app": module_label}, ) request.addfinalizer(envoy.delete) envoy.commit() return envoy +@pytest.fixture(scope="module") +def exposer(request) -> Exposer: + """Exposer object instance with TLS passthrough""" + exposer = OpenShiftExposer(passthrough=True) + request.addfinalizer(exposer.delete) + exposer.commit() + return exposer + + @pytest.fixture(scope="module") def authorino_parameters(authorino_parameters, authorino_cert, create_secret): """Setup TLS for authorino""" diff --git a/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_attributes.py b/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_attributes.py index a9419661..192468e8 100644 --- a/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_attributes.py +++ b/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_attributes.py @@ -13,15 +13,15 @@ def authorization(authorization, blame, cert_attributes): return authorization -def test_mtls_multiple_attributes_success(envoy_authority, valid_cert, route): +def test_mtls_multiple_attributes_success(envoy_authority, valid_cert, hostname): """Test successful mtls authentication with two matching attributes""" - with route.client(verify=envoy_authority, cert=valid_cert) as client: + with hostname.client(verify=envoy_authority, cert=valid_cert) as client: response = client.get("/get") assert response.status_code == 200 -def test_mtls_multiple_attributes_fail(envoy_authority, custom_cert, route): +def test_mtls_multiple_attributes_fail(envoy_authority, custom_cert, hostname): """Test mtls authentication with one matched and one unmatched attributes""" - with route.client(verify=envoy_authority, cert=custom_cert) as client: + with hostname.client(verify=envoy_authority, cert=custom_cert) as client: response = client.get("/get") assert response.status_code == 403 diff --git a/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_identity.py b/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_identity.py index 3fd4313b..cafb3505 100644 --- a/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_identity.py +++ b/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_identity.py @@ -3,9 +3,9 @@ from httpx import ReadError, ConnectError -def test_mtls_success(envoy_authority, valid_cert, route): +def test_mtls_success(envoy_authority, valid_cert, hostname): """Test successful mtls authentication""" - with route.client(verify=envoy_authority, cert=valid_cert) as client: + with hostname.client(verify=envoy_authority, cert=valid_cert) as client: response = client.get("/get") assert response.status_code == 200 @@ -21,18 +21,18 @@ def test_mtls_success(envoy_authority, valid_cert, route): ), ], ) -def test_mtls_fail(request, cert_authority, certificate, err, err_match: str, route): +def test_mtls_fail(request, cert_authority, certificate, err, err_match: str, hostname): """Test failed mtls verification""" ca = request.getfixturevalue(cert_authority) cert = request.getfixturevalue(certificate) if certificate else None with pytest.raises(err, match=err_match): - with route.client(verify=ca, cert=cert) as client: + with hostname.client(verify=ca, cert=cert) as client: client.get("/get") -def test_mtls_unmatched_attributes(envoy_authority, custom_cert, route): +def test_mtls_unmatched_attributes(envoy_authority, custom_cert, hostname): """Test certificate that signed by the trusted CA, though their attributes are unmatched""" - with route.client(verify=envoy_authority, cert=custom_cert) as client: + with hostname.client(verify=envoy_authority, cert=custom_cert) as client: response = client.get("/get") assert response.status_code == 403 diff --git a/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_trust_chain.py b/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_trust_chain.py index e3080db7..18b92cf9 100644 --- a/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_trust_chain.py +++ b/testsuite/tests/kuadrant/authorino/operator/tls/mtls/test_mtls_trust_chain.py @@ -9,22 +9,22 @@ def create_intermediate_authority_secrets(create_secret, authorino_labels, certi create_secret(certificates["intermediate_ca_unlabeled"], "interunld") -def test_mtls_trust_chain_success(envoy_authority, certificates, route): +def test_mtls_trust_chain_success(envoy_authority, certificates, hostname): """Test mtls verification with certificate signed by intermediate authority in the trust chain""" - with route.client(verify=envoy_authority, cert=certificates["intermediate_valid_cert"]) as client: + with hostname.client(verify=envoy_authority, cert=certificates["intermediate_valid_cert"]) as client: response = client.get("/get") assert response.status_code == 200 -def test_mtls_trust_chain_fail(envoy_authority, certificates, route): +def test_mtls_trust_chain_fail(envoy_authority, certificates, hostname): """Test mtls verification on intermediate certificate with unmatched attribute""" - with route.client(verify=envoy_authority, cert=certificates["intermediate_custom_cert"]) as client: + with hostname.client(verify=envoy_authority, cert=certificates["intermediate_custom_cert"]) as client: response = client.get("/get") assert response.status_code == 403 -def test_mtls_trust_chain_rejected_cert(envoy_authority, certificates, route): +def test_mtls_trust_chain_rejected_cert(envoy_authority, certificates, hostname): """Test mtls verification with intermediate certificate accepted in Envoy, but rejected by Authorino""" - with route.client(verify=envoy_authority, cert=certificates["intermediate_cert_unlabeled"]) as client: + with hostname.client(verify=envoy_authority, cert=certificates["intermediate_cert_unlabeled"]) as client: response = client.get("/get") assert response.status_code == 401 diff --git a/testsuite/tests/kuadrant/authorino/operator/tls/test_tls.py b/testsuite/tests/kuadrant/authorino/operator/tls/test_tls.py index d8d041a6..96c7e23a 100644 --- a/testsuite/tests/kuadrant/authorino/operator/tls/test_tls.py +++ b/testsuite/tests/kuadrant/authorino/operator/tls/test_tls.py @@ -3,22 +3,22 @@ from httpx import ReadError -def test_valid_certificate(envoy_authority, valid_cert, auth, route): +def test_valid_certificate(envoy_authority, valid_cert, auth, hostname): """Tests that valid certificate will be accepted""" - with route.client(verify=envoy_authority, cert=valid_cert) as client: + with hostname.client(verify=envoy_authority, cert=valid_cert) as client: response = client.get("/get", auth=auth) assert response.status_code == 200 -def test_no_certificate(route, envoy_authority): +def test_no_certificate(hostname, envoy_authority): """Test that request without certificate will be rejected""" with pytest.raises(ReadError, match="certificate required"): - with route.client(verify=envoy_authority) as client: + with hostname.client(verify=envoy_authority) as client: client.get("/get") -def test_invalid_certificate(envoy_authority, invalid_cert, auth, route): +def test_invalid_certificate(envoy_authority, invalid_cert, auth, hostname): """Tests that certificate with different CA will be rejeceted""" with pytest.raises(ReadError, match="unknown ca"): - with route.client(verify=envoy_authority, cert=invalid_cert) as client: + with hostname.client(verify=envoy_authority, cert=invalid_cert) as client: client.get("/get", auth=auth) diff --git a/testsuite/tests/kuadrant/authorino/wristband/conftest.py b/testsuite/tests/kuadrant/authorino/wristband/conftest.py index 22a0b9b5..902c15c4 100644 --- a/testsuite/tests/kuadrant/authorino/wristband/conftest.py +++ b/testsuite/tests/kuadrant/authorino/wristband/conftest.py @@ -1,12 +1,12 @@ """Conftest for Edge Authentication tests""" -from importlib import resources import pytest -from testsuite.objects import WristbandSigningKeyRef, WristbandResponse -from testsuite.openshift.objects.auth_config import AuthConfig -from testsuite.openshift.envoy import Envoy from testsuite.certificates import CertInfo +from testsuite.objects import WristbandResponse, WristbandSigningKeyRef +from testsuite.openshift.objects.auth_config import AuthConfig +from testsuite.openshift.objects.envoy.route import EnvoyVirtualRoute +from testsuite.openshift.objects.envoy.wristband import WristbandEnvoy from testsuite.openshift.objects.secret import TLSSecret from testsuite.utils import cert_builder @@ -45,17 +45,14 @@ def certificates(cfssl, wildcard_domain): @pytest.fixture(scope="module") -def proxy(request, authorino, openshift, blame, backend, module_label, testconfig): +def gateway(request, authorino, openshift, blame, module_label, testconfig): """Deploys Envoy with additional edge-route match""" - wristband_envoy = resources.files("testsuite.resources.wristband").joinpath("envoy.yaml") - envoy = Envoy( + envoy = WristbandEnvoy( openshift, + blame("gw"), authorino, - blame("envoy"), - module_label, - backend, - testconfig["envoy"]["image"], - template=wristband_envoy, + testconfig["service_protection"]["envoy"]["image"], + labels={"app": module_label}, ) request.addfinalizer(envoy.delete) envoy.commit() @@ -90,18 +87,24 @@ def wristband_token(client, auth): @pytest.fixture(scope="module") -def authenticated_route(proxy, blame): +def authenticated_route(exposer, gateway, blame): """Second envoy route, intended for the already authenticated user""" - return proxy.expose_hostname(blame("route-authenticated")) + return exposer.expose_hostname(blame("route"), gateway) @pytest.fixture(scope="module") -def authenticated_authorization(openshift, blame, authenticated_route, module_label, wristband_endpoint): +def authenticated_authorization(request, gateway, blame, authenticated_route, module_label, wristband_endpoint): """Second AuthConfig with authorino oidc endpoint, protecting route for the already authenticated user""" + route = EnvoyVirtualRoute.create_instance(gateway.openshift, blame("route"), gateway) + route.add_hostname(authenticated_route.hostname) + + request.addfinalizer(route.delete) + route.commit() + authorization = AuthConfig.create_instance( - openshift, + gateway.openshift, blame("auth-authenticated"), - authenticated_route, + route, labels={"testRun": module_label}, ) authorization.identity.add_oidc("edge-authenticated", wristband_endpoint) diff --git a/testsuite/tests/kuadrant/conftest.py b/testsuite/tests/kuadrant/conftest.py index a81911ed..8163bd12 100644 --- a/testsuite/tests/kuadrant/conftest.py +++ b/testsuite/tests/kuadrant/conftest.py @@ -66,11 +66,3 @@ def commit(request, authorization, rate_limit): if component is not None: request.addfinalizer(component.delete) component.commit() - - -@pytest.fixture(scope="module") -def client(route): - """Returns httpx client to be used for requests, it also commits AuthConfig""" - client = route.client() - yield client - client.close() diff --git a/testsuite/tests/kuadrant/reconciliation/test_httproute_delete.py b/testsuite/tests/kuadrant/reconciliation/test_httproute_delete.py index 111ade57..db3f37ab 100644 --- a/testsuite/tests/kuadrant/reconciliation/test_httproute_delete.py +++ b/testsuite/tests/kuadrant/reconciliation/test_httproute_delete.py @@ -3,7 +3,7 @@ @pytest.mark.issue("https://github.com/Kuadrant/kuadrant-operator/issues/124") -def test_delete(client, authorization, resilient_request): +def test_delete(client, route, authorization, resilient_request): """ Tests that after deleting HTTPRoute, status.conditions shows it missing: * Test that that client works @@ -16,7 +16,7 @@ def test_delete(client, authorization, resilient_request): response = client.get("/get") assert response.status_code == 200 - authorization.route.delete() + route.delete() response = resilient_request("/get", http_client=client, expected_status=404) assert response.status_code == 404, "Removing HTTPRoute was not reconciled" diff --git a/testsuite/tests/kuadrant/reconciliation/test_httproute_hosts.py b/testsuite/tests/kuadrant/reconciliation/test_httproute_hosts.py index 98ecb903..b80c679c 100644 --- a/testsuite/tests/kuadrant/reconciliation/test_httproute_hosts.py +++ b/testsuite/tests/kuadrant/reconciliation/test_httproute_hosts.py @@ -3,20 +3,20 @@ @pytest.fixture -def second_route(proxy, blame): +def second_hostname(exposer, gateway, blame): """Add a second hostname to a HTTPRoute""" - return proxy.expose_hostname(blame("second")) + return exposer.expose_hostname(blame("second"), gateway) @pytest.fixture -def client2(second_route): +def client2(second_hostname): """Client for a second hostname to HTTPRoute""" - client = second_route.client() + client = second_hostname.client() yield client client.close() -def test_add_host(client, client2, second_route, authorization, resilient_request): +def test_add_host(client, client2, second_hostname, route, resilient_request): """ Tests that HTTPRoute spec.hostnames changes are reconciled when changed: * Test that both hostnames work @@ -25,6 +25,7 @@ def test_add_host(client, client2, second_route, authorization, resilient_reques * Add back second hostname * Test that second hostname works """ + route.add_hostname(second_hostname.hostname) response = client.get("/get") assert response.status_code == 200 @@ -32,12 +33,12 @@ def test_add_host(client, client2, second_route, authorization, resilient_reques response = client2.get("/get") assert response.status_code == 200, "Adding host was not reconciled" - authorization.remove_host(second_route.hostname) + route.remove_hostname(second_hostname.hostname) response = resilient_request("/get", http_client=client2, expected_status=404) assert response.status_code == 404, "Removing host was not reconciled" - authorization.add_host(second_route.hostname) + route.add_hostname(second_hostname.hostname) response = resilient_request("/get", http_client=client2) assert response.status_code == 200, "Adding host was not reconciled" diff --git a/testsuite/tests/kuadrant/reconciliation/test_httproute_matches.py b/testsuite/tests/kuadrant/reconciliation/test_httproute_matches.py index 9d886a30..601a2833 100644 --- a/testsuite/tests/kuadrant/reconciliation/test_httproute_matches.py +++ b/testsuite/tests/kuadrant/reconciliation/test_httproute_matches.py @@ -1,7 +1,7 @@ """Tests that HTTPRoute spec.routes.matches changes are reconciled when changed.""" -def test_matches(client, route, resilient_request): +def test_matches(client, backend, route, resilient_request): """ Tests that HTTPRoute spec.routes.matches changes are reconciled when changed * Test that /get works @@ -12,7 +12,7 @@ def test_matches(client, route, resilient_request): response = client.get("/get") assert response.status_code == 200 - route.set_match(path_prefix="/anything") + route.set_match(backend, path_prefix="/anything") response = resilient_request("/get", expected_status=404) assert response.status_code == 404, "Matches were not reconciled" diff --git a/testsuite/tests/mgc/conftest.py b/testsuite/tests/mgc/conftest.py index 49be7ebf..c04229bd 100644 --- a/testsuite/tests/mgc/conftest.py +++ b/testsuite/tests/mgc/conftest.py @@ -3,25 +3,15 @@ from openshift import selector from weakget import weakget -from testsuite.openshift.httpbin import Httpbin +from testsuite.objects.gateway import GatewayRoute, CustomReference +from testsuite.objects.hostname import Exposer from testsuite.openshift.objects.dnspolicy import DNSPolicy -from testsuite.openshift.objects.gateway_api import CustomReference -from testsuite.openshift.objects.gateway_api.gateway import MGCGateway, GatewayProxy +from testsuite.openshift.objects.gateway_api.gateway import MGCGateway +from testsuite.openshift.objects.gateway_api.hostname import DNSPolicyExposer from testsuite.openshift.objects.gateway_api.route import HTTPRoute -from testsuite.openshift.objects.proxy import Proxy -from testsuite.openshift.objects.route import Route from testsuite.openshift.objects.tlspolicy import TLSPolicy -@pytest.fixture(scope="module") -def backend(request, gateway, blame, label): - """Deploys Httpbin backend""" - httpbin = Httpbin(gateway.openshift, blame("httpbin"), label) - request.addfinalizer(httpbin.delete) - httpbin.commit() - return httpbin - - @pytest.fixture(scope="session") def spokes(testconfig): """Returns Map of spokes names and their respective clients""" @@ -40,39 +30,24 @@ def hub_openshift(testconfig): @pytest.fixture(scope="module") -def upstream_gateway(request, hub_openshift, blame, hostname, module_label): - """Creates and returns configured and ready upstream Gateway""" - upstream_gateway = MGCGateway.create_instance( +def hub_gateway(request, hub_openshift, blame, base_domain, module_label) -> MGCGateway: + """Creates and returns configured and ready Hub Gateway""" + hub_gateway = MGCGateway.create_instance( openshift=hub_openshift, name=blame("mgc-gateway"), gateway_class="kuadrant-multi-cluster-gateway-instance-per-cluster", - hostname=f"*.{hostname}", + hostname=f"*.{base_domain}", tls=True, placement="http-gateway", labels={"app": module_label}, ) - request.addfinalizer(upstream_gateway.delete_tls_secret) # pylint: disable=no-member - request.addfinalizer(upstream_gateway.delete) - upstream_gateway.commit() + request.addfinalizer(hub_gateway.delete_tls_secret) # pylint: disable=no-member + request.addfinalizer(hub_gateway.delete) + hub_gateway.commit() # we cannot wait here because of referencing not yet existent tls secret which would be provided later by tlspolicy # upstream_gateway.wait_for_ready() - return upstream_gateway - - -@pytest.fixture(scope="module") -def proxy(request, gateway, backend, module_label) -> Proxy: - """Deploys Envoy that wire up the Backend behind the reverse-proxy and Authorino instance""" - envoy: Proxy = GatewayProxy(gateway, module_label, backend) - request.addfinalizer(envoy.delete) - envoy.commit() - return envoy - - -@pytest.fixture(scope="module") -def initial_host(hostname): - """Hostname that will be added to HTTPRoute""" - return f"route.{hostname}" + return hub_gateway @pytest.fixture(scope="session") @@ -86,28 +61,29 @@ def self_signed_cluster_issuer(): @pytest.fixture(scope="module") -def route(request, proxy, blame, gateway, initial_host, backend) -> Route: - """Exposed Route object""" - route = HTTPRoute.create_instance( - gateway.openshift, - blame("route"), - gateway, - initial_host, - backend, - labels={"app": proxy.label}, - ) +def route(request, gateway, blame, hostname, backend, module_label) -> GatewayRoute: + """Route object""" + route = HTTPRoute.create_instance(gateway.openshift, blame("route"), gateway, {"app": module_label}) + route.add_hostname(hostname.hostname) + route.add_backend(backend) request.addfinalizer(route.delete) route.commit() return route +@pytest.fixture(scope="module") +def exposer(base_domain, hub_gateway) -> Exposer: + """DNSPolicyExposer setup with expected TLS certificate""" + return DNSPolicyExposer(base_domain, tls_cert=hub_gateway.get_tls_cert()) + + # pylint: disable=unused-argument @pytest.fixture(scope="module") -def gateway(upstream_gateway, spokes, hub_policies_commit): +def gateway(hub_gateway, spokes, hub_policies_commit): """Downstream gateway, e.g. gateway on a spoke cluster""" # wait for upstream gateway here to be able to get spoke gateways - upstream_gateway.wait_for_ready() - gw = upstream_gateway.get_spoke_gateway(spokes) + hub_gateway.wait_for_ready() + gw = hub_gateway.get_spoke_gateway(spokes) gw.wait_for_ready() return gw @@ -128,27 +104,19 @@ def base_domain(request, hub_openshift): @pytest.fixture(scope="module") -def hostname(blame, base_domain): - """Returns domain used for testing""" - return f"{blame('mgc')}.{base_domain}" - - -@pytest.fixture(scope="module") -def dns_policy(blame, upstream_gateway, module_label): +def dns_policy(blame, hub_gateway, module_label): """DNSPolicy fixture""" - policy = DNSPolicy.create_instance( - upstream_gateway.openshift, blame("dns"), upstream_gateway, labels={"app": module_label} - ) + policy = DNSPolicy.create_instance(hub_gateway.openshift, blame("dns"), hub_gateway, labels={"app": module_label}) return policy @pytest.fixture(scope="module") -def tls_policy(blame, upstream_gateway, module_label, self_signed_cluster_issuer): +def tls_policy(blame, hub_gateway, module_label, self_signed_cluster_issuer): """TLSPolicy fixture""" policy = TLSPolicy.create_instance( - upstream_gateway.openshift, + hub_gateway.openshift, blame("tls"), - parent=upstream_gateway, + parent=hub_gateway, issuer=self_signed_cluster_issuer, labels={"app": module_label}, ) @@ -156,7 +124,7 @@ def tls_policy(blame, upstream_gateway, module_label, self_signed_cluster_issuer @pytest.fixture(scope="module") -def hub_policies_commit(request, upstream_gateway, dns_policy, tls_policy): +def hub_policies_commit(request, hub_gateway, dns_policy, tls_policy): """Commits all important stuff before tests""" for component in [dns_policy, tls_policy]: if component is not None: diff --git a/testsuite/tests/mgc/dnspolicy/conftest.py b/testsuite/tests/mgc/dnspolicy/conftest.py index fa728c35..14a27bdb 100644 --- a/testsuite/tests/mgc/dnspolicy/conftest.py +++ b/testsuite/tests/mgc/dnspolicy/conftest.py @@ -5,21 +5,21 @@ @pytest.fixture(scope="module") -def upstream_gateway(request, hub_openshift, blame, hostname, module_label): +def hub_gateway(request, hub_openshift, blame, base_domain, module_label): """Creates and returns configured and ready upstream Gateway with disabled tls""" - upstream_gateway = MGCGateway.create_instance( + hub_gateway = MGCGateway.create_instance( openshift=hub_openshift, name=blame("mgc-gateway"), gateway_class="kuadrant-multi-cluster-gateway-instance-per-cluster", - hostname=f"*.{hostname}", + hostname=f"*.{base_domain}", tls=False, placement="http-gateway", labels={"app": module_label}, ) - request.addfinalizer(upstream_gateway.delete) - upstream_gateway.commit() + request.addfinalizer(hub_gateway.delete) + hub_gateway.commit() - return upstream_gateway + return hub_gateway @pytest.fixture(scope="module") diff --git a/testsuite/tests/mgc/dnspolicy/health_check/conftest.py b/testsuite/tests/mgc/dnspolicy/health_check/conftest.py index 5b003f96..c50b0377 100644 --- a/testsuite/tests/mgc/dnspolicy/health_check/conftest.py +++ b/testsuite/tests/mgc/dnspolicy/health_check/conftest.py @@ -2,28 +2,43 @@ import time import pytest +from testsuite.objects.hostname import Hostname from testsuite.openshift.objects.gateway_api.gateway import MGCGateway @pytest.fixture(scope="module") -def upstream_gateway(request, hub_openshift, blame, module_label, initial_host): +def name(blame): + """Hostname that will be added to HTTPRoute""" + return blame("hostname") + + +@pytest.fixture(scope="module") +def hostname(gateway, exposer, name) -> Hostname: + """Exposed Hostname object""" + hostname = exposer.expose_hostname(name, gateway) + return hostname + + +@pytest.fixture(scope="module") +def hub_gateway(request, hub_openshift, blame, name, base_domain, module_label): """ Creates and returns configured and ready upstream Gateway with FQDN hostname Health checks available only with Fully Qualified Domain Names in gateway (no wildcards are allowed) """ - upstream_gateway = MGCGateway.create_instance( + hub_gateway = MGCGateway.create_instance( openshift=hub_openshift, name=blame("mgc-gateway"), gateway_class="kuadrant-multi-cluster-gateway-instance-per-cluster", - hostname=initial_host, + # This relies on exact naming scheme in DNSPolicyExposer, workaround for circular dependency + hostname=f"{name}.{base_domain}", tls=False, placement="http-gateway", labels={"app": module_label}, ) - request.addfinalizer(upstream_gateway.delete) - upstream_gateway.commit() + request.addfinalizer(hub_gateway.delete) + hub_gateway.commit() - return upstream_gateway + return hub_gateway @pytest.fixture(scope="module") diff --git a/testsuite/tests/mgc/test_basic.py b/testsuite/tests/mgc/test_basic.py index 82661d63..926d4e05 100644 --- a/testsuite/tests/mgc/test_basic.py +++ b/testsuite/tests/mgc/test_basic.py @@ -1,22 +1,14 @@ """ -This module contains the very basic tests and their dependencies for MGC +This module contains the most basic happy path test for both DNSPolicy and TLSPolicy Prerequisites: -* the hub cluster is also a spoke cluster so that everything happens on the only cluster * multi-cluster-gateways ns is created and set as openshift["project"] * managedclustersetbinding is created in openshift["project"] -* placement named "local-cluster" is created in openshift["project"] and bound to clusterset * gateway class "kuadrant-multi-cluster-gateway-instance-per-cluster" is created -* openshift2["project"] is set -Notes: -* dnspolicies are created and bound to gateways automatically by mgc operator -* dnspolicies leak at this moment """ import pytest -from testsuite.httpx import KuadrantClient - pytestmark = [pytest.mark.mgc] @@ -25,18 +17,13 @@ def test_gateway_readiness(gateway): assert gateway.is_ready() -def test_smoke(route, upstream_gateway): +def test_smoke(client): """ Tests whether the backend, exposed using the HTTPRoute and Gateway, was exposed correctly, having a tls secured endpoint with a hostname managed by MGC """ - tls_cert = upstream_gateway.get_tls_cert() - - # assert that tls_cert is used by the server - backend_client = KuadrantClient(base_url=f"https://{route.hostnames[0]}", verify=tls_cert) - - result = backend_client.get("get") + result = client.get("/get") assert not result.has_dns_error() assert not result.has_tls_error() assert result.status_code == 200 diff --git a/testsuite/tests/mgc/tlspolicy/test_cert_parameters.py b/testsuite/tests/mgc/tlspolicy/test_cert_parameters.py index b060792d..0d633ede 100644 --- a/testsuite/tests/mgc/tlspolicy/test_cert_parameters.py +++ b/testsuite/tests/mgc/tlspolicy/test_cert_parameters.py @@ -28,9 +28,9 @@ def tls_policy(tls_policy): @pytest.fixture(scope="module") -def tls_cert(upstream_gateway, gateway): # pylint: disable=unused-argument +def tls_cert(hub_gateway, gateway): # pylint: disable=unused-argument """Return certificate generated by TLSPolicy""" - return upstream_gateway.get_tls_cert() + return hub_gateway.get_tls_cert() def test_tls_cert_common_name(tls_cert):