Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 9 additions & 23 deletions testsuite/component_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
28 changes: 28 additions & 0 deletions testsuite/kubernetes/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"""
Expand Down Expand Up @@ -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:
Expand Down
18 changes: 10 additions & 8 deletions testsuite/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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)
Expand Down
40 changes: 28 additions & 12 deletions testsuite/tests/info_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,30 @@

logger = logging.getLogger(__name__)


def _first_connected(namespace):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please remind me why this collector wasn't collecting information from every cluster, and is using only the first one it finds available?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our multicluster pipeline, all clusters share the same configuration. They're deployed with the same kuadrant-operator image, so the component images and versions are identical across clusters. The launch attributes this collector gathers (mostly Kuadrant component images extracted from the operator) would be duplicated if we collected from every cluster. Using only one cluster avoids that redundancy while still capturing all the relevant information.
This approach can change in the future if we need cluster-specific properties, but right now collecting from multiple clusters would only result in duplicate attributes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still prefer collecting the real information from each cluster, even if collect task would take 3x time.

I don't like that this already confusing process is getting refactored with the new confusing algorithm, I need to also be aware of now.

This might be the moment to change this, and assign None values to missing configurations instead of blindly trusting the install pipelines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to pull @zkraus into this conversation to get his opinion as well. My concern is that collecting attributes from every cluster would result in unnecessary duplication (since all clusters share the same operator image, the attributes would be identical). We'd either have duplicate keys or need cluster-prefixed names (e.g. cluster1_kuadrant-operator, cluster2_kuadrant-operator), which makes the launch attributes harder to read and filter on in Report Portal without adding useful information.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirm, that the reason was to collect only one, because of assumption of unified environment.
But there should be no harm of collecting it. It will be preparation, if/when we start testing interoperability.

If the versions would be equal, I would not set duplicated attributes (if that is even possible), so maybe checking if there is already equal key and value. This should be simple enough, and we can change that if necessary.

On the other hand, as we will be getting the version and any data, from each cluster -- which is definitely beneficial -- please do use logger, and log the information collected per cluster in the collection test. Log as much as you think would be valuable. In the log, it is accessible, readable, and will not hurt anyone. will not overuse attributes. I think that is nearly ideal place to put a large amount of information, in case we need it later. -- Next upgrade, would be to put this data as an attachment in json/yaml format (next time).

TL;DR: I agree, collect info from all clusters, deduplicate attributes, log everything.

"""Return the first (cluster_client, project) connected to the given namespace, or (None, None)."""
for _, cluster in ReportPortalMetadataCollector.get_cluster_configurations():
project = cluster.change_project(namespace)
if project.connected:
return cluster, project
return None, None


pytestmark = pytest.mark.skipif(
not os.environ.get("COLLECTOR_ENABLE"),
reason="collector was not explicitly enabled",
)


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"])
"""Gather cluster versions from the first cluster with Kuadrant system namespace."""
cluster, project = _first_connected(settings["service_protection"]["system_project"])
if project is None:
return {}
return {
"kubernetes": ReportPortalMetadataCollector.get_kubernetes_version(project),
"openshift": ReportPortalMetadataCollector.get_ocp_version(project),
"openshift": cluster.ocp_version,
}


Expand Down Expand Up @@ -79,17 +90,21 @@ def test_cluster_properties(record_testsuite_property):


def test_kube_context(record_testsuite_property):
"""Record current kube context"""
kube_context = settings["control_plane"]["cluster"].kubeconfig_path
"""Record kube context from the first cluster with kuadrant-system."""
cluster_client, _ = _first_connected(settings["service_protection"]["system_project"])
if cluster_client is None:
return
kube_context = cluster_client.kubeconfig_path
print(f"{kube_context=}")
if kube_context:
record_testsuite_property("kube_context", kube_context)


def test_kuadrant_properties(record_testsuite_property):
"""Record kuadrant related properties"""
cluster_client = settings["control_plane"]["cluster"]
project = cluster_client.change_project("kuadrant-system")
"""Record kuadrant related properties from the first cluster with kuadrant-system."""
_, project = _first_connected(settings["service_protection"]["system_project"])
if project is None:
return
kuadrant_images = ReportPortalMetadataCollector.get_component_images(project)
if kuadrant_images:
print(f"Kuadrant images: {kuadrant_images}")
Expand All @@ -98,9 +113,10 @@ def test_kuadrant_properties(record_testsuite_property):


def test_istio_properties(record_testsuite_property):
"""Record Istio related properties"""
cluster_client = settings["control_plane"]["cluster"]
project = cluster_client.change_project("istio-system")
"""Record Istio related properties from the first cluster with istio-system."""
_, project = _first_connected("istio-system")
if project is None:
return
istio_metadata = ReportPortalMetadataCollector.get_istio_metadata(project)
for key, value in istio_metadata.items():
print(f"{key}: {value}")
Expand Down
Loading