diff --git a/testsuite/component_metadata.py b/testsuite/component_metadata.py index e4ad17a0..aa6a7cb1 100644 --- a/testsuite/component_metadata.py +++ b/testsuite/component_metadata.py @@ -21,13 +21,14 @@ def __init__(self): def collect_all_clusters(self): """Collect metadata from all configured clusters.""" - clusters_config = self._get_cluster_configurations() + clusters_config = self.get_cluster_configurations() for cluster_name, cluster_client in clusters_config: metadata = self._collect_single_cluster(cluster_client) if metadata: self.all_cluster_metadata[cluster_name] = metadata - def _get_cluster_configurations(self): + @staticmethod + def get_cluster_configurations(): """Get cluster configurations from settings.""" clusters_config = [("cluster1", settings["control_plane"]["cluster"])] if cluster2 := settings["control_plane"].get("cluster2"): @@ -38,14 +39,16 @@ def _get_cluster_configurations(self): def _collect_single_cluster(self, cluster_client): """Collect metadata for a single cluster.""" - project = cluster_client.change_project(settings["service_protection"]["system_project"]) - if not project.connected: + if not cluster_client.is_reachable: return None + project = cluster_client.change_project(settings["service_protection"]["system_project"]) + metadata = self._get_kuadrant_metadata(project) if project.connected else {"kuadrant_image": "not installed"} + return { - "metadata": self._get_kuadrant_metadata(project), + "metadata": metadata, "console_url": self._get_console_url(cluster_client.api_url), - "ocp_version": self.get_ocp_version(project), + "ocp_version": cluster_client.ocp_version, } @staticmethod @@ -78,23 +81,6 @@ def _get_console_url(api_url): return f"https://{console_hostname}" return api_url - @staticmethod - def get_ocp_version(project) -> Optional[str]: - """Retrieve and format OCP version from cluster.""" - try: - with project.context: - version_result = oc.selector("clusterversion").objects() - if version_result: - ocp_version = version_result[0].model.status.history[0].version - if ocp_version: - parts = ocp_version.split(".") - if len(parts) >= 2: - return f"{parts[0]}.{parts[1]}" - except (oc.OpenShiftPythonException, AttributeError, KeyError, IndexError, ValueError) as e: - logger.warning("Failed to get OCP version: %s", e) - - return None - @staticmethod def get_kubernetes_version(project) -> Optional[str]: """Run oc version and get the kubernetes version.""" @@ -130,13 +116,12 @@ def get_component_images(project) -> list[tuple]: if normalised_image in seen: continue seen.add(normalised_image) - print(f"{image=}") image_name = normalised_image.split("/")[-1] if ":" in image_name: name, tag = image_name.rsplit(":", 1) images.append((name, tag, image)) else: - logger.debug("Skipping image without tag: %s", image) + images.append((image_name, None, image)) except (oc.OpenShiftPythonException, AttributeError, KeyError, IndexError, ValueError) as e: logger.warning("Failed to get images from %s: %s", project, e) diff --git a/testsuite/kubernetes/client.py b/testsuite/kubernetes/client.py index 88f175d3..0210c6db 100644 --- a/testsuite/kubernetes/client.py +++ b/testsuite/kubernetes/client.py @@ -1,6 +1,8 @@ """This module implements an KubernetesCLI interface using oc/kubectl binary commands.""" +import logging from functools import cached_property +from typing import Optional from urllib.parse import urlparse import tempfile import yaml @@ -14,6 +16,8 @@ from .deployment import Deployment from .secret import Secret +logger = logging.getLogger(__name__) + class KubernetesClient: """KubernetesClient is a helper class for invoking kubectl commands""" @@ -100,6 +104,30 @@ def connected(self): return False return True + @property + def is_reachable(self): + """Returns True if the cluster is reachable, without depending on any specific namespace.""" + try: + self.do_action("api-versions") + except OpenShiftPythonException as e: + logger.warning("Cluster is not reachable: %s", e) + return False + return True + + @property + def ocp_version(self) -> Optional[str]: + """Returns the OpenShift version (major.minor) or None if not available.""" + result = self.do_action( + "get", "clusterversion", "version", "-o", "jsonpath={.status.history[0].version}", auto_raise=False + ) + if result.status() != 0: + return None + version_str = result.out().strip() + parts = version_str.split(".") + if len(parts) >= 2: + return f"{parts[0]}.{parts[1]}" + return None + def get_secret(self, name): """Returns dict-like structure for accessing secret data""" with self.context: diff --git a/testsuite/tests/conftest.py b/testsuite/tests/conftest.py index 903533c4..5a22b872 100644 --- a/testsuite/tests/conftest.py +++ b/testsuite/tests/conftest.py @@ -45,6 +45,10 @@ def pytest_runtest_setup(item): # error is raised during has_kuadrant() if item.config.getoption("--setup-plan"): return + + if item.fspath.basename == "info_collector.py": + return + marks = [i.name for i in item.iter_markers()] skip_or_fail = pytest.fail if item.config.getoption("--enforce") else pytest.skip standalone = item.config.getoption("--standalone") @@ -422,16 +426,14 @@ def dns_provider_secret(testconfig): @pytest.fixture(scope="session") -def openshift_version(cluster): +def openshift_version(testconfig): """Get OpenShift cluster version""" - result = cluster.do_action( - "get", "clusterversion", "version", "-o", "jsonpath={.status.desired.version}", auto_raise=False - ) - if result.status() != 0: + cluster = testconfig["control_plane"]["cluster"] + version = cluster.ocp_version + if version is None: return None - version_str = result.out().strip() - parts = version_str.split(".") - return tuple(int(p.split("-")[0]) for p in parts[:2]) # Convert "4.20.0" -> (4, 20) + parts = version.split(".") + return tuple(int(p.split("-")[0]) for p in parts[:2]) # Convert "4.20" -> (4, 20) @pytest.fixture(autouse=True) diff --git a/testsuite/tests/info_collector.py b/testsuite/tests/info_collector.py index 4939c60e..b2b0b95c 100644 --- a/testsuite/tests/info_collector.py +++ b/testsuite/tests/info_collector.py @@ -36,14 +36,31 @@ ) -def gather_cluster_versions() -> dict: - """gather all particular versions into a dictionary""" - cluster_client = settings["control_plane"]["cluster"] - project = cluster_client.change_project(settings["service_protection"]["system_project"]) - return { - "kubernetes": ReportPortalMetadataCollector.get_kubernetes_version(project), - "openshift": ReportPortalMetadataCollector.get_ocp_version(project), - } +def _all_cluster_projects(namespace): + """Yield (cluster_name, cluster_client, project or None) for each configured cluster.""" + for cluster_name, cluster in ReportPortalMetadataCollector.get_cluster_configurations(): + project = cluster.change_project(namespace) + yield cluster_name, cluster, (project if project.connected else None) + + +def _print_cluster_data(cluster_data): + """Print collected data per cluster.""" + for cluster_name, lines in cluster_data.items(): + print(f"\n{cluster_name}:") + for line in lines: + print(f" {line}") + + +def _record_unique(record_testsuite_property, properties): + """Record properties, only adding unique ones as attributes.""" + seen = set() + for key, value in properties: + if not value: + continue + if (key, value) not in seen: + seen.add((key, value)) + record_testsuite_property(key, value) + logger.info("recording property %s:%s", key, value) def get_cluster_information() -> dict: @@ -70,39 +87,62 @@ def test_launch_description(record_testsuite_property): def test_cluster_properties(record_testsuite_property): - """Collect cluster properties""" - cluster_versions = gather_cluster_versions() - for k, v in cluster_versions.items(): - print(f"recording property {k}:{v}") - if v: # filter out None values - record_testsuite_property(k, v) - - -def test_kube_context(record_testsuite_property): - """Record current kube context""" - kube_context = settings["control_plane"]["cluster"].kubeconfig_path - print(f"{kube_context=}") - if kube_context: - record_testsuite_property("kube_context", kube_context) + """Collect cluster version properties from all clusters.""" + system_ns = settings["service_protection"]["system_project"] + properties = [] + cluster_data = {} + for cluster_name, cluster, project in _all_cluster_projects(system_ns): + if project is None: + cluster_data[cluster_name] = [f"namespace '{system_ns}' not found"] + continue + versions = { + "kubernetes": ReportPortalMetadataCollector.get_kubernetes_version(project), + "openshift": cluster.ocp_version, + } + cluster_data[cluster_name] = [f"{k}:{v}" for k, v in versions.items()] + for key, value in versions.items(): + properties.append((key, value)) + + _print_cluster_data(cluster_data) + _record_unique(record_testsuite_property, properties) def test_kuadrant_properties(record_testsuite_property): - """Record kuadrant related properties""" - cluster_client = settings["control_plane"]["cluster"] - project = cluster_client.change_project("kuadrant-system") - kuadrant_images = ReportPortalMetadataCollector.get_component_images(project) - if kuadrant_images: - print(f"Kuadrant images: {kuadrant_images}") - for name, tag, _ in kuadrant_images: - record_testsuite_property(name, tag) + """Record kuadrant related properties from all clusters.""" + system_ns = settings["service_protection"]["system_project"] + properties = [] + cluster_data = {} + for cluster_name, _, project in _all_cluster_projects(system_ns): + if project is None: + cluster_data[cluster_name] = [f"namespace '{system_ns}' not found"] + continue + cluster_data[cluster_name] = [] + kuadrant_images = ReportPortalMetadataCollector.get_component_images(project) + for name, tag, full_image in kuadrant_images: + if tag: + cluster_data[cluster_name].append(f"{name}:{tag} ({full_image})") + properties.append((name, tag)) + else: + cluster_data[cluster_name].append(full_image) + + _print_cluster_data(cluster_data) + _record_unique(record_testsuite_property, properties) def test_istio_properties(record_testsuite_property): - """Record Istio related properties""" - cluster_client = settings["control_plane"]["cluster"] - project = cluster_client.change_project("istio-system") - istio_metadata = ReportPortalMetadataCollector.get_istio_metadata(project) - for key, value in istio_metadata.items(): - print(f"{key}: {value}") - if key == "istio_version": - record_testsuite_property(key, value) + """Record Istio related properties from all clusters.""" + properties = [] + cluster_data = {} + for cluster_name, _, project in _all_cluster_projects("istio-system"): + if project is None: + cluster_data[cluster_name] = ["namespace 'istio-system' not found"] + continue + cluster_data[cluster_name] = [] + istio_metadata = ReportPortalMetadataCollector.get_istio_metadata(project) + for key, value in istio_metadata.items(): + cluster_data[cluster_name].append(f"{key}:{value}") + if key == "istio_version": + properties.append((key, value)) + + _print_cluster_data(cluster_data) + _record_unique(record_testsuite_property, properties)