From ba13072f3ac79eb779a7bdbf0a7df353cb9c35a0 Mon Sep 17 00:00:00 2001 From: phala Date: Tue, 25 Jun 2024 14:31:02 +0200 Subject: [PATCH] Add Kuadrantctl tests --- Makefile | 14 ++-- config/settings.local.yaml.tpl | 1 + config/settings.yaml | 1 + testsuite/gateway/gateway_api/route.py | 12 ++++ testsuite/kuadrantctl.py | 52 +++++++++++++++ testsuite/oas.py | 49 ++++++++++++++ testsuite/openshift/client.py | 9 +++ testsuite/resources/oas/__init__.py | 0 testsuite/resources/oas/base_httpbin.yaml | 35 ++++++++++ testsuite/tests/kuadrantctl/__init__.py | 0 testsuite/tests/kuadrantctl/cli/__init__.py | 0 .../kuadrantctl/cli/test_basic_commands.py | 12 ++++ .../tests/kuadrantctl/cli/test_simple_auth.py | 66 +++++++++++++++++++ .../kuadrantctl/cli/test_simple_limit.py | 45 +++++++++++++ .../kuadrantctl/cli/test_simple_route.py | 66 +++++++++++++++++++ testsuite/tests/kuadrantctl/conftest.py | 48 ++++++++++++++ 16 files changed, 405 insertions(+), 5 deletions(-) create mode 100644 testsuite/kuadrantctl.py create mode 100644 testsuite/oas.py create mode 100644 testsuite/resources/oas/__init__.py create mode 100644 testsuite/resources/oas/base_httpbin.yaml create mode 100644 testsuite/tests/kuadrantctl/__init__.py create mode 100644 testsuite/tests/kuadrantctl/cli/__init__.py create mode 100644 testsuite/tests/kuadrantctl/cli/test_basic_commands.py create mode 100644 testsuite/tests/kuadrantctl/cli/test_simple_auth.py create mode 100644 testsuite/tests/kuadrantctl/cli/test_simple_limit.py create mode 100644 testsuite/tests/kuadrantctl/cli/test_simple_route.py create mode 100644 testsuite/tests/kuadrantctl/conftest.py diff --git a/Makefile b/Makefile index eb773082..9a40c688 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: commit-acceptance pylint mypy black reformat test authorino poetry poetry-no-dev mgc container-image polish-junit reportportal authorino-standalone limitador kuadrant kuadrant-only disruptive +.PHONY: commit-acceptance pylint mypy black reformat test authorino poetry poetry-no-dev mgc container-image polish-junit reportportal authorino-standalone limitador kuadrant kuadrant-only disruptive kuadrantctl TB ?= short LOGLEVEL ?= INFO @@ -45,7 +45,7 @@ test pytest tests: kuadrant authorino: ## Run only authorino related tests authorino: poetry-no-dev - $(PYTEST) -n4 -m 'authorino' --dist loadfile --enforce $(flags) testsuite + $(PYTEST) -n4 -m 'authorino' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant authorino-standalone: ## Run only test capable of running with standalone Authorino authorino-standalone: poetry-no-dev @@ -53,15 +53,15 @@ authorino-standalone: poetry-no-dev limitador: ## Run only Limitador related tests limitador: poetry-no-dev - $(PYTEST) -n4 -m 'limitador' --dist loadfile --enforce $(flags) testsuite + $(PYTEST) -n4 -m 'limitador' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant kuadrant: ## Run all tests available on Kuadrant kuadrant: poetry-no-dev - $(PYTEST) -n4 -m 'not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite + $(PYTEST) -n4 -m 'not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant kuadrant-only: ## Run Kuadrant-only tests kuadrant-only: poetry-no-dev - $(PYTEST) -n4 -m 'kuadrant_only and not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite + $(PYTEST) -n4 -m 'kuadrant_only and not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant dnstls: ## Run DNS and TLS tests dnstls: poetry-no-dev @@ -71,6 +71,10 @@ disruptive: ## Run disruptive tests disruptive: poetry-no-dev $(PYTEST) -m 'disruptive' $(flags) testsuite +kuadrantctl: ## Run Kuadrantctl tests +kuadrantctl: poetry-no-dev + $(PYTEST) -n4 --dist loadfile --enforce $(flags) testsuite/tests/kuadrantctl + poetry.lock: pyproject.toml poetry lock diff --git a/config/settings.local.yaml.tpl b/config/settings.local.yaml.tpl index 413e16c2..81f89c85 100644 --- a/config/settings.local.yaml.tpl +++ b/config/settings.local.yaml.tpl @@ -5,6 +5,7 @@ # api_url: "https://api.openshift.com" # Optional: OpenShift API URL, if None it will OpenShift that you are logged in # token: "KUADRANT_RULEZ" # Optional: OpenShift Token, if None it will OpenShift that you are logged in # kubeconfig_path: "~/.kube/config" # Optional: Kubeconfig to use, if None the default one is used +# kuadrantctl: kuadrantctl # tools: # project: "tools" # Optional: OpenShift project, where external tools are located # keycloak: diff --git a/config/settings.yaml b/config/settings.yaml index c6426e31..fb1da0e1 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -1,6 +1,7 @@ default: dynaconf_merge: true cluster: {} + kuadrantctl: "kuadrantctl" tools: project: "tools" cfssl: "cfssl" diff --git a/testsuite/gateway/gateway_api/route.py b/testsuite/gateway/gateway_api/route.py index 902e5d9c..714458b0 100644 --- a/testsuite/gateway/gateway_api/route.py +++ b/testsuite/gateway/gateway_api/route.py @@ -114,3 +114,15 @@ def add_backend(self, backend: "Backend", prefix="/"): @modify def remove_all_backend(self): self.model.spec.rules.clear() + + def wait_for_ready(self): + """Waits until HTTPRoute is reconcilled by GatewayProvider""" + + def _ready(obj): + for condition_set in obj.model.status.parents: + if condition_set.controllerName == "istio.io/gateway-controller": + return (all(x.status == "True" for x in condition_set.conditions),) + return False + + success = self.wait_until(_ready, timelimit=10) + assert success, f"{self.kind()} did got get ready in time" diff --git a/testsuite/kuadrantctl.py b/testsuite/kuadrantctl.py new file mode 100644 index 00000000..f8e8995b --- /dev/null +++ b/testsuite/kuadrantctl.py @@ -0,0 +1,52 @@ +# pylint: disable=line-too-long +""" +Help as of 0.2.3 +Kuadrant configuration command line utility + +Usage: + kuadrantctl [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + generate Commands related to kubernetes object generation + gatewayapi Generate Gataway API resources + httproute Generate Gateway API HTTPRoute from OpenAPI 3.0.X + kuadrant Generate Kuadrant resources + authpolicy Generate Kuadrant AuthPolicy from OpenAPI 3.0.X + ratelimitpolicy Generate Kuadrant Rate Limit Policy from OpenAPI 3.0.X + + + help Help about any command + version Print the version number of kuadrantctl + +Flags: + -h, --help help for httproute + --oas string Path to OpenAPI spec file (in JSON or YAML format), URL, or '-' to read from standard input (required) + -o, --output-format string Output format: 'yaml' or 'json'. (default "yaml") + +Global Flags: + -v, --verbose verbose output + + +Use "kuadrantctl [command] --help" for more information about a command. + +""" + +import subprocess + + +class KuadrantCTL: + """Wrapper on top of kuadrantctl binary""" + + def __init__(self, binary) -> None: + super().__init__() + self.binary = binary + + def run(self, *args, **kwargs): + """Passes arguments to Subprocess.run, see that for more details""" + args = (self.binary, *args) + kwargs.setdefault("capture_output", True) + kwargs.setdefault("check", True) + kwargs.setdefault("text", True) + # We do supply value for check :) + return subprocess.run(args, **kwargs) # pylint: disable= subprocess-run-check diff --git a/testsuite/oas.py b/testsuite/oas.py new file mode 100644 index 00000000..306dcce3 --- /dev/null +++ b/testsuite/oas.py @@ -0,0 +1,49 @@ +"""OAS processing""" + +import contextlib +import json +import tempfile +from collections import UserDict + +import yaml + +from testsuite.backend import Backend +from testsuite.gateway import Referencable, Hostname + + +@contextlib.contextmanager +def as_tmp_file(text): + """Saves text in a temporary file and returns absolute path""" + with tempfile.NamedTemporaryFile("w") as file: + file.write(text) + file.flush() + yield file.name + + +class OASWrapper(UserDict): + """Wrapper for OpenAPISpecification""" + + def as_json(self): + """Returns OAS as JSON""" + return json.dumps(self.data) + + def as_yaml(self): + """Returns OAS as YAML""" + return yaml.dump(self.data) + + def add_backend_to_paths(self, backend: Backend): + """Adds backend to all paths, should be only used in tests that do not test this section""" + for path in self["paths"].values(): + path["x-kuadrant"] = { + "backendRefs": [backend.reference], + } + + def add_top_level_route(self, parent: Referencable, hostname: Hostname, name: str): + """Adds top-level x-kuadrant definition for Route, should be only used in tests that do not test this section""" + self["x-kuadrant"] = { + "route": { + "name": name, + "hostnames": [hostname.hostname], + "parentRefs": [parent.reference], + } + } diff --git a/testsuite/openshift/client.py b/testsuite/openshift/client.py index 92d61707..ad0b0c3c 100644 --- a/testsuite/openshift/client.py +++ b/testsuite/openshift/client.py @@ -119,3 +119,12 @@ def project_exists(self): return True except oc.OpenShiftPythonException: return False + + def apply_from_string(self, string, cls, cmd_args=None): + """Applies new object from the string to the server and returns it wrapped in the class""" + with self.context: + selector = oc.apply(string, cmd_args=cmd_args) + obj = selector.object(cls=cls) + obj.context = self.context + obj.committed = True + return obj diff --git a/testsuite/resources/oas/__init__.py b/testsuite/resources/oas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/resources/oas/base_httpbin.yaml b/testsuite/resources/oas/base_httpbin.yaml new file mode 100644 index 00000000..4ef74b6a --- /dev/null +++ b/testsuite/resources/oas/base_httpbin.yaml @@ -0,0 +1,35 @@ +--- +openapi: 3.1.0 +info: + title: Httpbin + version: 0.0.51 +paths: + "/get": + get: + operationId: get_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: string + "/anything": + get: + operationId: get_anything + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: string + put: + operationId: put_anything + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: string \ No newline at end of file diff --git a/testsuite/tests/kuadrantctl/__init__.py b/testsuite/tests/kuadrantctl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/tests/kuadrantctl/cli/__init__.py b/testsuite/tests/kuadrantctl/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/tests/kuadrantctl/cli/test_basic_commands.py b/testsuite/tests/kuadrantctl/cli/test_basic_commands.py new file mode 100644 index 00000000..e2134e43 --- /dev/null +++ b/testsuite/tests/kuadrantctl/cli/test_basic_commands.py @@ -0,0 +1,12 @@ +"""Tests basic commands""" + +import pytest + + +# https://github.com/Kuadrant/kuadrantctl/issues/90 +@pytest.mark.parametrize("command", ["help", "version"]) +def test_commands(kuadrantctl, command): + """Test that basic commands exists and returns anything""" + result = kuadrantctl.run(command) + assert not result.stderr, f"Command '{command}' returned an error: {result.stderr}" + assert result.stdout, f"Command '{command}' returned empty output" diff --git a/testsuite/tests/kuadrantctl/cli/test_simple_auth.py b/testsuite/tests/kuadrantctl/cli/test_simple_auth.py new file mode 100644 index 00000000..6e04aa60 --- /dev/null +++ b/testsuite/tests/kuadrantctl/cli/test_simple_auth.py @@ -0,0 +1,66 @@ +"""Tests that you can generate simple AuthPolicy, focused on the cmdline options more than on extension functionality""" + +import pytest + +from testsuite.httpx.auth import HttpxOidcClientAuth +from testsuite.oas import as_tmp_file +from testsuite.policy.authorization.auth_policy import AuthPolicy + + +@pytest.fixture(scope="module") +def auth(keycloak): + """Returns authentication object for HTTPX""" + return HttpxOidcClientAuth(keycloak.get_token, "authorization") + + +@pytest.fixture(scope="module") +def oas(oas, keycloak, blame, gateway, hostname, backend): + """Add OIDC configuration""" + oas.add_top_level_route(gateway, hostname, blame("route")) + + oas["components"] = { + "securitySchemes": { + "oidc": { + "type": "openIdConnect", + "openIdConnectUrl": keycloak.well_known["issuer"], + # https://github.com/Kuadrant/kuadrantctl/issues/94 + # "openIdConnectUrl": keycloak.well_known["issuer"] + "/.well-known/openid-configuration", + } + } + } + anything = oas["paths"]["/anything"] + anything["x-kuadrant"] = { + "backendRefs": [backend.reference], + } + anything["get"]["security"] = [{"oidc": []}] + return oas + + +@pytest.mark.parametrize("encoder", [pytest.param("as_json", id="JSON"), pytest.param("as_yaml", id="YAML")]) +@pytest.mark.parametrize("stdin", [pytest.param(True, id="STDIN"), pytest.param(False, id="File")]) +def test_generate_authpolicy(request, kuadrantctl, oas, encoder, openshift, client, stdin, auth): + """Generates Policy from OAS and tests that it works as expected""" + encoded = getattr(oas, encoder)() + + if stdin: + result = kuadrantctl.run("generate", "kuadrant", "authpolicy", "--oas", "-", input=encoded) + else: + with as_tmp_file(encoded) as file_name: + result = kuadrantctl.run("generate", "kuadrant", "authpolicy", "--oas", file_name) + + policy = openshift.apply_from_string(result.stdout, AuthPolicy) + request.addfinalizer(policy.delete) + + policy.wait_for_ready() + + response = client.get("/anything") + assert response.status_code == 401 + + response = client.get("/anything", auth=auth) + assert response.status_code == 200 + + response = client.get("/anything", headers={"Authorization": "Bearer xyz"}) + assert response.status_code == 401 + + response = client.put("/anything") + assert response.status_code == 200 diff --git a/testsuite/tests/kuadrantctl/cli/test_simple_limit.py b/testsuite/tests/kuadrantctl/cli/test_simple_limit.py new file mode 100644 index 00000000..68f17741 --- /dev/null +++ b/testsuite/tests/kuadrantctl/cli/test_simple_limit.py @@ -0,0 +1,45 @@ +""" +Tests that you can generate simple RateLimitPolicy, focused on the cmdline options more than on extension +functionality +""" + +import pytest + +from testsuite.oas import as_tmp_file +from testsuite.policy.rate_limit_policy import Limit, RateLimitPolicy +from testsuite.utils import asdict + + +@pytest.fixture(scope="module") +def oas(oas, blame, gateway, hostname, backend): + """Add X-Kuadrant specific fields""" + oas.add_top_level_route(gateway, hostname, blame("route")) + oas.add_backend_to_paths(backend) + + oas["paths"]["/anything"]["get"]["x-kuadrant"] = {"rate_limit": {"rates": [asdict(Limit(3, 20))]}} + return oas + + +@pytest.mark.parametrize("encoder", [pytest.param("as_json", id="JSON"), pytest.param("as_yaml", id="YAML")]) +@pytest.mark.parametrize("stdin", [pytest.param(True, id="STDIN"), pytest.param(False, id="File")]) +def test_generate_limit(request, kuadrantctl, oas, encoder, openshift, client, stdin): + """Tests that RateLimitPolicy can be generated and that it is enforced as expected""" + encoded = getattr(oas, encoder)() + + if stdin: + result = kuadrantctl.run("generate", "kuadrant", "ratelimitpolicy", "--oas", "-", input=encoded) + else: + with as_tmp_file(encoded) as file_name: + result = kuadrantctl.run("generate", "kuadrant", "ratelimitpolicy", "--oas", file_name) + + policy = openshift.apply_from_string(result.stdout, RateLimitPolicy) + request.addfinalizer(policy.delete) + policy.wait_for_ready() + + responses = client.get_many("/anything", 3) + responses.assert_all(status_code=200) + assert client.get("/anything").status_code == 429 + + # Check that it did not affect other endpoints + responses = client.get_many("/get", 5) + responses.assert_all(status_code=200) diff --git a/testsuite/tests/kuadrantctl/cli/test_simple_route.py b/testsuite/tests/kuadrantctl/cli/test_simple_route.py new file mode 100644 index 00000000..ee4eff6b --- /dev/null +++ b/testsuite/tests/kuadrantctl/cli/test_simple_route.py @@ -0,0 +1,66 @@ +"""Tests that you can generate simple HTTPRoute, focused on the cmdline options more than on extension functionality""" + +import pytest + +from testsuite.gateway.gateway_api.route import HTTPRoute +from testsuite.oas import as_tmp_file + + +@pytest.fixture(scope="module") +def route(): + """Make sure Route is not created automatically""" + return None + + +@pytest.fixture(scope="module") +def oas(oas, blame, gateway, hostname, backend): + """Add Route and Backend specifications to OAS""" + oas["x-kuadrant"] = { + "route": { + "name": blame("route"), + "hostnames": [hostname.hostname], + "parentRefs": [gateway.reference], + } + } + oas.add_backend_to_paths(backend) + return oas + + +@pytest.mark.parametrize("encoder", [pytest.param("as_json", id="JSON"), pytest.param("as_yaml", id="YAML")]) +@pytest.mark.parametrize("stdin", [pytest.param(True, id="STDIN"), pytest.param(False, id="File")]) +def test_generate_route(request, kuadrantctl, oas, encoder, openshift, client, stdin): + """Tests that Route can be generated and that is works as expected""" + encoded = getattr(oas, encoder)() + + if stdin: + result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", "-", input=encoded) + else: + with as_tmp_file(encoded) as file_name: + result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", file_name) + + # https://github.com/Kuadrant/kuadrantctl/issues/91 + route = openshift.apply_from_string(result.stdout, HTTPRoute, cmd_args="--validate=false") + # route = openshift.apply_from_string(result.stdout, HTTPRoute) + request.addfinalizer(route.delete) + route.wait_for_ready() + + response = client.get("/get") + assert response.status_code == 200 + + response = client.get("/anything") + assert response.status_code == 200 + + response = client.put("/anything") + assert response.status_code == 200 + + # Incorrect methods + response = client.post("/anything") + assert response.status_code == 404 + + # Incorrect path + response = client.get("/anything/test") + assert response.status_code == 404 + + # Incorrect endpoint + response = client.post("/post") + assert response.status_code == 404 diff --git a/testsuite/tests/kuadrantctl/conftest.py b/testsuite/tests/kuadrantctl/conftest.py new file mode 100644 index 00000000..9b63789d --- /dev/null +++ b/testsuite/tests/kuadrantctl/conftest.py @@ -0,0 +1,48 @@ +"""Conftest for kuadrantctl tests""" + +import shutil +from importlib import resources + +import pytest +import yaml + +from testsuite.gateway.gateway_api.route import HTTPRoute +from testsuite.kuadrantctl import KuadrantCTL +from testsuite.oas import OASWrapper + + +@pytest.fixture(scope="session") +def kuadrantctl(testconfig, skip_or_fail): + """Return Kuadrantctl wrapper""" + binary_path = testconfig["kuadrantctl"] + if not shutil.which(binary_path): + skip_or_fail("Kuadrantctl binary not found") + return KuadrantCTL(binary_path) + + +@pytest.fixture(scope="module") +def oas(): + """ + OpenAPISpecification definition + """ + return OASWrapper( + yaml.safe_load(resources.files("testsuite.resources.oas").joinpath("base_httpbin.yaml").read_text()) + ) + + +@pytest.fixture(scope="function") +def route(request, kuadrantctl, oas, openshift): + """Generates Route from OAS""" + result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", "-", input=oas.as_yaml(), check=False) + assert result.returncode == 0, f"Unable to create Route from OAS: {result.stderr}" + route = openshift.apply_from_string(result.stdout, HTTPRoute, cmd_args="--validate=false") + request.addfinalizer(route.delete) + return route + + +@pytest.fixture(scope="function") +def client(hostname, route): # pylint: disable=unused-argument + """Returns httpx client to be used for requests, it also commits AuthConfig""" + client = hostname.client() + yield client + client.close()