diff --git a/config/settings.local.yaml.tpl b/config/settings.local.yaml.tpl index 54d6ef4b3..58ca66881 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 09e3eb19a..0869fc205 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 902e5d9c5..714458b05 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 000000000..567889210 --- /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: + """The doc string""" + + 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 000000000..116a30b70 --- /dev/null +++ b/testsuite/oas.py @@ -0,0 +1,20 @@ +"""OAS processing""" + +import json +from collections import UserDict + +import yaml + + +def as_json(oas): + """Returns OAS as JSON""" + return json.dumps(oas.data) + + +def as_yaml(oas): + """Returns OAS as JSON""" + return yaml.dump(oas.data) + + +class OASWrapper(UserDict): + """Simple wrapper for OAS""" diff --git a/testsuite/resources/oas/__init__.py b/testsuite/resources/oas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/testsuite/resources/oas/base_httpbin.yaml b/testsuite/resources/oas/base_httpbin.yaml new file mode 100644 index 000000000..4ef74b6ab --- /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 000000000..e69de29bb diff --git a/testsuite/tests/kuadrantctl/cli/__init__.py b/testsuite/tests/kuadrantctl/cli/__init__.py new file mode 100644 index 000000000..e69de29bb 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 000000000..e2134e438 --- /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 000000000..ff9535581 --- /dev/null +++ b/testsuite/tests/kuadrantctl/cli/test_simple_auth.py @@ -0,0 +1,86 @@ +"""Tests that you can generate simple AuthPolicy, focused on the cmdline options more than on extension functionality""" + +import tempfile + +import pytest +from openshift_client import apply + +from testsuite.httpx.auth import HttpxOidcClientAuth +from testsuite.oas import as_json, as_yaml +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 base_oas(base_oas, keycloak): + """Add OIDC configuration""" + base_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", + } + } + } + return base_oas + + +@pytest.fixture(scope="module") +def oas_kuadrant(base_oas, blame, gateway, hostname, backend): + """Add X-Kuadrant specific fields""" + base_oas["info"]["x-kuadrant"] = { + "route": { + "name": blame("route"), + "hostnames": [hostname.hostname], + "parentRefs": [gateway.reference], + } + } + + anything = base_oas["paths"]["/anything"] + anything["x-kuadrant"] = { + "backendRefs": [backend.reference], + } + anything["get"]["security"] = [{"oidc": []}] + return base_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 = encoder(oas) + + if stdin: + result = kuadrantctl.run("generate", "kuadrant", "authpolicy", "--oas", "-", input=encoded) + else: + with tempfile.NamedTemporaryFile("w") as file: + file.write(encoded) + file.flush() + result = kuadrantctl.run("generate", "kuadrant", "authpolicy", "--oas", file.name) + + with openshift.context: + selector = apply(result.stdout) + request.addfinalizer(selector.delete) + policy = selector.object(cls=AuthPolicy) + policy.committed = True + + 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_route.py b/testsuite/tests/kuadrantctl/cli/test_simple_route.py new file mode 100644 index 000000000..ebf58c248 --- /dev/null +++ b/testsuite/tests/kuadrantctl/cli/test_simple_route.py @@ -0,0 +1,78 @@ +"""Tests that you can generate simple HTTPRoute, focused on the cmdline options more than on extension functionality""" + +import tempfile + +import pytest +from openshift_client import apply + +from testsuite.gateway.gateway_api.route import HTTPRoute +from testsuite.oas import as_json, as_yaml + + +@pytest.fixture(scope="module") +def route(): + """Make sure Route is not created automatically""" + return None + + +@pytest.fixture(scope="module") +def oas_kuadrant(base_oas, blame, gateway, hostname, backend): + """Add Route and Backend specifications to OAS""" + base_oas["info"]["x-kuadrant"] = { + "route": { + "name": blame("route"), + "hostnames": [hostname.hostname], + "parentRefs": [gateway.reference], + } + } + for path in base_oas["paths"].values(): + path["x-kuadrant"] = { + "backendRefs": [backend.reference], + } + return base_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 = encoder(oas) + + if stdin: + result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", "-", input=encoded) + else: + with tempfile.NamedTemporaryFile("w") as file: + file.write(encoded) + file.flush() + result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", file.name) + + with openshift.context: + # https://github.com/Kuadrant/kuadrantctl/issues/91 + selector = apply(result.stdout, cmd_args="--validate=false") + # selector = apply(result.stdout) + request.addfinalizer(selector.delete) + route = selector.object(cls=HTTPRoute) + route.committed = True + + 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 000000000..8e9ecf295 --- /dev/null +++ b/testsuite/tests/kuadrantctl/conftest.py @@ -0,0 +1,63 @@ +"""Conftest for kuadrantctl tests""" + +import shutil +from importlib import resources + +import pytest +import yaml +from openshift_client import apply + +from testsuite.gateway.gateway_api.route import HTTPRoute +from testsuite.kuadrantctl import KuadrantCTL +from testsuite.oas import OASWrapper, as_yaml + + +@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 base_oas(): + """ + Pure OAS, base should be loaded from the resources and securityScheme should be added manually to suit the test + """ + return OASWrapper( + yaml.safe_load(resources.files("testsuite.resources.oas").joinpath("base_httpbin.yaml").read_text()) + ) + + +@pytest.fixture(scope="module") +def oas_kuadrant(base_oas): + """Used for adding X-kuadrant extension values to the OAS""" + return base_oas + + +@pytest.fixture(scope="module") +def oas(oas_kuadrant): + """Shortcut to final version of OAS, should not be overridden""" + return oas_kuadrant + + +@pytest.fixture(scope="module") +def route(request, kuadrantctl, oas, openshift): + """Generates Route from OAS""" + result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", "-", input=as_yaml(oas)) + with openshift.context: + selector = apply(result.stdout, cmd_args="--validate=false") + request.addfinalizer(selector.delete) + route = selector.object(cls=HTTPRoute) + route.committed = True + return route + + +@pytest.fixture(scope="module") +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()