From 147df9db3e436f9cb769a5113ce33751ee6d3f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Mon, 10 Jun 2024 12:31:59 +0200 Subject: [PATCH] tests: test CRD versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test which checks if the CRD versions defined in the cluster match the versions in the subcharts in the git repo. This test ensures that the subcharts in the release version don't unexpectedly change their API versions within a minor Harvester release. To implement this test, additional infrastructure for the Kubernetes API has been added. fixes: https://github.com/harvester/tests/issues/1314 Signed-off-by: Moritz Röhrich --- apiclient/kube_api/__init__.py | 1 + apiclient/kube_api/api.py | 70 ++++++++++++++++ harvester_e2e_tests/apis/test_crd_versions.py | 81 +++++++++++++++++++ .../fixtures/kube_api_client.py | 20 +++++ test-requirements.txt | 2 + 5 files changed, 174 insertions(+) create mode 100644 apiclient/kube_api/__init__.py create mode 100644 apiclient/kube_api/api.py create mode 100644 harvester_e2e_tests/apis/test_crd_versions.py create mode 100644 harvester_e2e_tests/fixtures/kube_api_client.py diff --git a/apiclient/kube_api/__init__.py b/apiclient/kube_api/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apiclient/kube_api/__init__.py @@ -0,0 +1 @@ + diff --git a/apiclient/kube_api/api.py b/apiclient/kube_api/api.py new file mode 100644 index 000000000..015ccaafd --- /dev/null +++ b/apiclient/kube_api/api.py @@ -0,0 +1,70 @@ +# Copyright (c) 2024 SUSE LLC +# +# pylint: disable=missing-function-docstring + +from urllib.parse import urljoin + +import kubernetes +import requests +import yaml + + +class KubeAPI: + """ + An abstraction of the Kubernetes API. + + Example usage: + + ``` + with KubeAPI(endpoint, tls_verify=false) as api: + api.authenticate(username, password, verify=false) + kube_client = api.get_client() + + corev1 = kubernetes.client.CoreV1Api(kube_client) + + namespaces = corev1.list_namespace() + ``` + """ + + HARVESTER_API_VERSION = "harvesterhci.io/v1beta1" + + def __init__(self, endpoint, tls_verify, token=None, session=None): + self.session = session or requests.Session() + self.session.verify = tls_verify + self.session.headers.update(Authorization=token or "") + + self.endpoint = endpoint + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, taceback): + pass + + def _post(self, path, **kwargs): + url = self._get_url(path) + return self.session.post(url, **kwargs) + + def _get_url(self, path): + return urljoin(self.endpoint, path).format(API_VERSION=self.HARVESTER_API_VERSION) + + def get_client(self): + path = "/v1/management.cattle.io.clusters/local" + params = {"action": "generateKubeconfig"} + + resp = self._post(path, params=params) + assert resp.status_code == 200, "Failed to generate kubeconfig" + + kubeconfig = yaml.safe_load(resp.json()['config']) + return kubernetes.config.new_client_from_config_dict(kubeconfig) + + def authenticate(self, user, passwd, **kwargs): + path = "v3-public/localProviders/local?action=login" + resp = self._post(path, json=dict(username=user, password=passwd), **kwargs) + + assert resp.status_code == 201, "Failed to authenticate" + + token = f"Bearer {resp.json()['token']}" + self.session.headers.update(Authorization=token) + + return resp.json() diff --git a/harvester_e2e_tests/apis/test_crd_versions.py b/harvester_e2e_tests/apis/test_crd_versions.py new file mode 100644 index 000000000..d4f5fbc48 --- /dev/null +++ b/harvester_e2e_tests/apis/test_crd_versions.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024 SUSE LLC +# +# pylint: disable=missing-function-docstring, redefined-outer-name + +from urllib.parse import urljoin + +import kubernetes +import pytest +import requests +import semver +import yaml + + +pytest_plugins = [ + "harvester_e2e_tests.fixtures.kube_api_client", + "harvester_e2e_tests.fixtures.api_client" +] + + +@pytest.fixture(scope="session") +def server_version(api_client): + code, data = api_client.settings.get(name="server-version") + assert code == 200 + assert data.get("value") is not None + + yield semver.VersionInfo.parse(data.get("value").lstrip("v")) + + +@pytest.fixture(scope="module", params=[ + ("csi-snapshotter", "volumesnapshotclasses"), + ("csi-snapshotter", "volumesnapshotcontents"), + ("csi-snapshotter", "volumesnapshots"), + ("kubevirt-operator", "crd-kubevirt"), + ("whereabouts", "whereabouts.cni.cncf.io_ippools"), + ("whereabouts", "whereabouts.cni.cncf.io_overlappingrangeipreservations") +]) +def chart_and_file_name(request): + yield request.param + + +@pytest.fixture(scope="module") +def expected_crd(server_version, chart_and_file_name): + raw_url = "https://raw.githubusercontent.com/harvester/harvester/" + raw_url = urljoin(raw_url, f"v{server_version.major}.{server_version.minor}/") + raw_url = urljoin(raw_url, "deploy/charts/harvester/dependency_charts/") + raw_url = urljoin(raw_url, f"{chart_and_file_name[0]}/") + raw_url = urljoin(raw_url, "crds/") + raw_url = urljoin(raw_url, f"{chart_and_file_name[1]}.yaml") + + resp = requests.get(raw_url, allow_redirects=True) + cont = resp.content.decode("utf-8") + data = yaml.safe_load(cont) + yield data + + +@pytest.fixture(scope="module") +def actual_crd(kube_api_client, expected_crd): + name = expected_crd['metadata']['name'] + kube_client = kubernetes.client.ApiextensionsV1Api(kube_api_client) + yield kube_client.read_custom_resource_definition(name=name) + + +@pytest.mark.api +def test_api_version(expected_crd, actual_crd): + expected_versions = [] + for ver in expected_crd['spec']['versions']: + expected_versions.append(ver['name']) + + actual_versions = [] + for ver in actual_crd.spec.versions: + actual_versions.append(ver.name) + + assert expected_crd['metadata']['name'] == actual_crd.metadata.name + + # Make sure all expected versions are there + for ver in expected_versions: + assert ver in actual_versions + + # Make sure all installed versions are expected + for ver in actual_versions: + assert ver in expected_versions diff --git a/harvester_e2e_tests/fixtures/kube_api_client.py b/harvester_e2e_tests/fixtures/kube_api_client.py new file mode 100644 index 000000000..970e02212 --- /dev/null +++ b/harvester_e2e_tests/fixtures/kube_api_client.py @@ -0,0 +1,20 @@ +# Copyright (c) 2024 SUSE LLC +# +# pylint: disable=missing-function-docstring + +import pytest + +from kube_api import KubeAPI + + +@pytest.fixture(scope="session") +def kube_api_client(request): + endpoint = request.config.getoption("--endpoint") + username = request.config.getoption("--username") + password = request.config.getoption("--password") + tls_verify = request.config.getoption("--ssl_verify", False) + + with KubeAPI(endpoint, tls_verify) as api: + api.authenticate(username, password, verify=tls_verify) + + yield api.get_client() diff --git a/test-requirements.txt b/test-requirements.txt index 187eea8df..eca6cfd29 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,6 +7,8 @@ pytest-json-report pytest-dependency jinja2 bcrypt +kubernetes +semver requests paramiko pycryptodome