-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RHOAIENG-16055: new(tests): test to start a Workbench, by creating the Notebook CR directly #94
base: main
Are you sure you want to change the base?
Changes from all commits
87c56c8
c696def
7199032
92744bb
5a79b41
67271a2
004d870
c1eddd8
5d73d41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,309 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use simple_logger |
||
import time | ||
import traceback | ||
from typing import Callable, Generator | ||
|
||
import kubernetes.dynamic | ||
from kubernetes.dynamic import DynamicClient | ||
|
||
import ocp_resources.pod | ||
import ocp_resources.resource | ||
|
||
import pytest | ||
|
||
|
||
@pytest.fixture(scope="function") | ||
def function_resource_manager(admin_client: DynamicClient) -> Generator[KubeResourceManager, None, None]: | ||
resource_manager = KubeResourceManager(admin_client) | ||
yield resource_manager | ||
resource_manager.destroy() | ||
|
||
|
||
class KubeResourceManager: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use ocpenshift python wrapper to CRUD resources |
||
def __init__(self, privileged_client: DynamicClient): | ||
self.privileged_client = privileged_client | ||
self.resources: list[ocp_resources.resource.Resource] = [] | ||
|
||
def createResourceWithoutWait(self, client: DynamicClient, resource: ocp_resources.resource.Resource): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use python naming conventions |
||
resource.client = self.privileged_client | ||
resource.create() | ||
self.resources.append(resource) | ||
|
||
def createResourceWithWait(self, client: DynamicClient, resource: ocp_resources.resource.Resource): | ||
resource.client = self.privileged_client | ||
resource.create() | ||
self.resources.append(resource) | ||
|
||
def destroy(self): | ||
for resource in self.resources: | ||
# self.privileged_client.delete(resource) | ||
resource.delete() | ||
|
||
|
||
class OdhAnnotationsLabels: | ||
OPENSHIFT_DOMAIN = "openshift.io/" | ||
ODH_DOMAIN = "opendatahub.io/" | ||
Comment on lines
+46
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. used in other places as well, please place under https://github.com/opendatahub-io/opendatahub-tests/blob/main/utilities/constants.py and re-use |
||
|
||
LABEL_DASHBOARD = ODH_DOMAIN + "dashboard" | ||
LABEL_ODH_MANAGED = ODH_DOMAIN + "odh-managed" | ||
LABEL_SIDECAR_ISTIO_INJECT = "sidecar.istio.io/inject" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
|
||
ANNO_SERVICE_MESH = ODH_DOMAIN + "service-mesh" | ||
ANNO_MODEL_MESH = "modelmesh-enabled" | ||
ANNO_NTB_INJECT_OAUTH = "notebooks." + ODH_DOMAIN + "inject-oauth" | ||
APP_LABEL_KEY = "app" | ||
APP_LABEL_VALUE = "odh-e2e" | ||
|
||
|
||
def getOdhOrRhoai(name, odh_value, rhoai_value): | ||
return rhoai_value | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use py_config["distrobution"] where needed |
||
|
||
|
||
class OdhConstants: | ||
# private static final Logger LOGGER = LoggerFactory.getLogger(OdhConstants.class); | ||
# private static final Map<String, String> VALUES = new HashMap<>(); | ||
Comment on lines
+65
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. leftovers? |
||
# // ODH | ||
ODH_CONTROLLERS_NAMESPACE: str = "opendatahub" | ||
ODH_DASHBOARD_ROUTE_NAME: str = "odh-dashboard" | ||
# private static final String ODH_DASHBOARD_CONTROLLER = "odh-dashboard"; | ||
# | ||
# private static final String ODH_BUNDLE_OPERATOR_NAME = "opendatahub-operator-system"; | ||
# private static final String ODH_MONITORING_NAMESPACE = "odh-monitoring"; | ||
# | ||
# | ||
# // ODH OLM | ||
# private static final String ODH_OLM_OPERATOR_NAME = "opendatahub-operator"; | ||
# private static final String ODH_OLM_OPERATOR_NAMESPACE = "openshift-operators"; | ||
# private static final String ODH_OLM_OPERATOR_DEPLOYMENT_NAME = "opendatahub-operator-controller-manager"; | ||
# private static final String ODH_OLM_SOURCE_NAME = "community-operators"; | ||
# private static final String ODH_OLM_APP_BUNDLE_PREFIX = "opendatahub-operator"; | ||
# private static final String ODH_OLM_OPERATOR_CHANNEL = "fast"; | ||
# private static final String ODH_DSCI_NAME = "default"; | ||
# // TODO - should be changed after 2.5 release | ||
# private static final String ODH_OLM_OPERATOR_VERSION = "v2.8.0"; | ||
# private static final String ODH_OLM_UPGRADE_STARTING_OPERATOR_VERSION = "v2.8.0"; | ||
# | ||
# // RHOAI | ||
RHOAI_CONTROLLERS_NAMESPACE: str = "redhat-ods-applications" | ||
RHOAI_DASHBOARD_ROUTE_NAME: str = "rhods-dashboard" | ||
# private static final String RHOAI_DASHBOARD_CONTROLLER = "rhods-dashboard"; | ||
# private static final String RHOAI_NOTEBOOKS_NAMESPACE = "rhods-notebooks"; | ||
# private static final String RHOAI_DSCI_NAME = "default-dsci"; | ||
# // RHOAI OLM | ||
# private static final String RHOAI_OLM_OPERATOR_NAME = "rhods-operator"; | ||
# private static final String RHOAI_OLM_OPERATOR_NAMESPACE = "redhat-ods-operator"; | ||
# private static final String RHOAI_OLM_OPERATOR_DEPLOYMENT_NAME = "rhods-operator"; | ||
# private static final String RHOAI_OLM_SOURCE_NAME = "redhat-operators"; | ||
# private static final String RHOAI_OLM_APP_BUNDLE_PREFIX = "rhods-operator"; | ||
# private static final String RHOAI_OLM_OPERATOR_CHANNEL = "fast"; | ||
# private static final String RHOAI_OLM_OPERATOR_VERSION = "2.7.0"; | ||
# private static final String RHOAI_OLM_UPGRADE_STARTING_OPERATOR_VERSION = "2.6.0"; | ||
# private static final String RHOAI_MONITORING_NAMESPACE = "redhat-ods-monitoring"; | ||
# | ||
# // Public part | ||
# public static final String DSC_CREATION_SUCCESSFUL_EVENT_NAME = "DataScienceClusterCreationSuccessful"; | ||
# | ||
# public static final String CODEFLARE_DEPLOYMENT_NAME = "codeflare-operator-manager"; | ||
# public static final String DS_PIPELINES_OPERATOR = "data-science-pipelines-operator-controller-manager"; | ||
# public static final String ETCD = "etcd"; | ||
# public static final String KSERVE_OPERATOR = "kserve-controller-manager"; | ||
# public static final String KUBERAY_OPERATOR = "kuberay-operator"; | ||
# public static final String MODELMESH_OPERATOR = "modelmesh-controller"; | ||
# public static final String NOTEBOOK_OPERATOR = "notebook-controller-deployment"; | ||
# public static final String ODH_MODEL_OPERATOR = "odh-model-controller"; | ||
# public static final String ODH_NOTEBOOK_OPERATOR = "odh-notebook-controller-manager"; | ||
# public static final String KUEUE_OPERATOR = "kueue-controller-manager"; | ||
# public static final String KNATIVE_SERVING_NAMESPACE = "knative-serving"; | ||
# public static final String ISTIO_SYSTEM_NAMESPACE = "istio-system"; | ||
# | ||
CONTROLLERS_NAMESPACE: str = getOdhOrRhoai( | ||
"CONTROLLERS_NAMESPACE", ODH_CONTROLLERS_NAMESPACE, RHOAI_CONTROLLERS_NAMESPACE | ||
) | ||
DASHBOARD_ROUTE_NAME: str = getOdhOrRhoai( | ||
"DASHBOARD_ROUTE_NAME", ODH_DASHBOARD_ROUTE_NAME, RHOAI_DASHBOARD_ROUTE_NAME | ||
) | ||
# public static final String DASHBOARD_CONTROLLER = getOdhOrRhoai("DASHBOARD_CONTROLLER", ODH_DASHBOARD_CONTROLLER, RHOAI_DASHBOARD_CONTROLLER); | ||
# public static final String NOTEBOOKS_NAMESPACE = getOdhOrRhoai("NOTEBOOKS_NAMESPACE", ODH_CONTROLLERS_NAMESPACE, RHOAI_NOTEBOOKS_NAMESPACE); | ||
# public static final String BUNDLE_OPERATOR_NAMESPACE = getOdhOrRhoai("BUNDLE_OPERATOR_NAMESPACE", ODH_BUNDLE_OPERATOR_NAME, RHOAI_OLM_OPERATOR_NAME); | ||
# public static final String DEFAULT_DSCI_NAME = getOdhOrRhoai("DSCI_NAME", ODH_DSCI_NAME, RHOAI_DSCI_NAME); | ||
# public static final String MONITORING_NAMESPACE = getOdhOrRhoai("MONITORING_NAMESPACE", ODH_MONITORING_NAMESPACE, RHOAI_MONITORING_NAMESPACE); | ||
# // OLM env variables | ||
# public static final String OLM_OPERATOR_NAME = getOdhOrRhoai("OLM_OPERATOR_NAME", ODH_OLM_OPERATOR_NAME, RHOAI_OLM_OPERATOR_NAME); | ||
# public static final String OLM_OPERATOR_NAMESPACE = getOdhOrRhoai("OLM_OPERATOR_NAMESPACE", ODH_OLM_OPERATOR_NAMESPACE, RHOAI_OLM_OPERATOR_NAMESPACE); | ||
# public static final String OLM_OPERATOR_DEPLOYMENT_NAME = getOdhOrRhoai("OLM_OPERATOR_DEPLOYMENT_NAME", ODH_OLM_OPERATOR_DEPLOYMENT_NAME, RHOAI_OLM_OPERATOR_DEPLOYMENT_NAME); | ||
# public static final String OLM_APP_BUNDLE_PREFIX = getOdhOrRhoai("OLM_APP_BUNDLE_PREFIX", ODH_OLM_APP_BUNDLE_PREFIX, RHOAI_OLM_APP_BUNDLE_PREFIX); | ||
# public static final String OLM_OPERATOR_VERSION = getOdhOrRhoai("OLM_OPERATOR_VERSION", ODH_OLM_OPERATOR_VERSION, RHOAI_OLM_OPERATOR_VERSION); | ||
# public static final String OLM_SOURCE_NAME = getOdhOrRhoai("OLM_SOURCE_NAME", ODH_OLM_SOURCE_NAME, RHOAI_OLM_SOURCE_NAME); | ||
# public static final String OLM_OPERATOR_CHANNEL = getOdhOrRhoai("OLM_OPERATOR_CHANNEL", ODH_OLM_OPERATOR_CHANNEL, RHOAI_OLM_OPERATOR_CHANNEL); | ||
# public static final String OLM_UPGRADE_STARTING_OPERATOR_VERSION = getOdhOrRhoai("OLM_UPGRADE_STARTING_OPERATOR_VERSION", ODH_OLM_UPGRADE_STARTING_OPERATOR_VERSION, RHOAI_OLM_UPGRADE_STARTING_OPERATOR_VERSION); | ||
|
||
|
||
class PodUtils: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use openshift python wrapper , e.g https://github.com/RedHatQE/openshift-python-wrapper/blob/main/ocp_resources/resource.py#L769 |
||
READINESS_TIMEOUT = 10 * 60 | ||
|
||
# consider using timeout_sampler | ||
@staticmethod | ||
def waitForPodsReady(client: DynamicClient, namespaceName: str, label_selector: str, expectPodsCount: int): | ||
"""Wait for all pods in namespace to be ready | ||
:param client: | ||
:param namespaceName: name of the namespace | ||
:param label_selector: | ||
:param expectPodsCount: | ||
""" | ||
|
||
# it's a dynamic client with the `resource` parameter already filled in | ||
class ResourceType(kubernetes.dynamic.Resource, kubernetes.dynamic.DynamicClient): | ||
pass | ||
|
||
resource: ResourceType = client.resources.get( | ||
kind=ocp_resources.pod.Pod.kind, | ||
api_version=ocp_resources.pod.Pod.api_version, | ||
) | ||
|
||
def ready() -> bool: | ||
pods = resource.get(namespace=namespaceName, label_selector=label_selector).items | ||
if not pods and expectPodsCount == 0: | ||
logging.debug("All expected Pods {} in Namespace {} are ready", label_selector, namespaceName) | ||
return True | ||
if not pods: | ||
logging.debug("Pods matching {}/{} are not ready", namespaceName, label_selector) | ||
return False | ||
if len(pods) != expectPodsCount: | ||
logging.debug("Expected Pods {}/{} are not ready", namespaceName, label_selector) | ||
return False | ||
for pod in pods: | ||
if not Readiness.isPodReady(pod) and not Readiness.isPodSucceeded(pod): | ||
logging.debug("Pod is not ready: {}/{}", namespaceName, pod.getMetadata().getName()) | ||
return False | ||
else: | ||
# check all containers in pods are ready | ||
for cs in pod.status.containerStatuses: | ||
if not (cs.ready or cs.state.get("terminated", {}).get("reason", "") == "Completed"): | ||
logging.debug( | ||
f"Container {cs.getName()} of Pod {namespaceName}/{pod.getMetadata().getName()} not ready" | ||
) | ||
return False | ||
logging.info("Pods matching {}/{} are ready", namespaceName, label_selector) | ||
return True | ||
|
||
Wait.until( | ||
f"readiness of all Pods matching {label_selector} in Namespace {namespaceName}", | ||
TestFrameConstants.GLOBAL_POLL_INTERVAL_MEDIUM, | ||
PodUtils.READINESS_TIMEOUT, | ||
ready, | ||
) | ||
|
||
|
||
class Wait: | ||
@staticmethod | ||
def until( | ||
description: str, | ||
pollInterval: float, | ||
timeout: float, | ||
ready: Callable[[], bool], | ||
onTimeout: Callable | None = None, | ||
): | ||
"""or every poll (happening once each {@code pollIntervalMs}) checks if supplier {@code ready} is true. | ||
# If yes, the wait is closed. Otherwise, waits another {@code pollIntervalMs} and tries again. | ||
# Once the wait timeout (specified by {@code timeoutMs} is reached and supplier wasn't true until that time, | ||
# runs the {@code onTimeout} (f.e. print of logs, showing the actual value that was checked inside {@code ready}), | ||
# and finally throws {@link WaitException}. | ||
# @param description information about on what we are waiting | ||
# @param pollIntervalMs poll interval in milliseconds | ||
# @param timeoutMs timeout specified in milliseconds | ||
# @param ready {@link BooleanSupplier} containing code, which should be executed each poll, | ||
# verifying readiness of the particular thing | ||
# @param onTimeout {@link Runnable} executed once timeout is reached and | ||
# before the {@link WaitException} is thrown.""" | ||
logging.info("Waiting for: {}", description) | ||
deadline = time.monotonic() + timeout | ||
|
||
exceptionMessage: str | None = None | ||
previousExceptionMessage: str | None = None | ||
|
||
# in case we are polling every 1s, we want to print exception after x tries, not on the first try | ||
# for minutes poll interval will 2 be enough | ||
exceptionAppearanceCount: int = 2 if (pollInterval // 60) > 0 else max(timeout // pollInterval // 4, 2) | ||
exceptionCount: int = 0 | ||
newExceptionAppearance: int = 0 | ||
|
||
stackTraceError: str | None = None | ||
|
||
while True: | ||
try: | ||
result: bool = ready() | ||
except Exception as e: | ||
exceptionMessage = str(e) | ||
|
||
exceptionCount += 1 | ||
newExceptionAppearance += 1 | ||
if ( | ||
exceptionCount == exceptionAppearanceCount | ||
and exceptionMessage is not None | ||
and exceptionMessage == previousExceptionMessage | ||
): | ||
logging.info(f"While waiting for: {description} exception occurred: {exceptionMessage}") | ||
# log the stacktrace | ||
stackTraceError = traceback.format_exc() | ||
elif ( | ||
exceptionMessage is not None | ||
and exceptionMessage != previousExceptionMessage | ||
and newExceptionAppearance == 2 | ||
): | ||
previousExceptionMessage = exceptionMessage | ||
|
||
result = False | ||
|
||
timeLeft: float = deadline - time.monotonic() | ||
if result: | ||
return | ||
if timeLeft <= 0: | ||
if exceptionCount > 1: | ||
logging.error("Exception waiting for: {}, {}", description, exceptionMessage) | ||
|
||
if stackTraceError is not None: | ||
# printing handled stacktrace | ||
logging.error(stackTraceError) | ||
if onTimeout is not None: | ||
onTimeout() | ||
waitException: WaitException = WaitException(f"Timeout after {timeout} s waiting for {description}") | ||
logging.error(waitException) | ||
raise waitException | ||
|
||
sleepTime: float = min(pollInterval, timeLeft) | ||
time.sleep(sleepTime) | ||
|
||
|
||
class WaitException(Exception): | ||
pass | ||
|
||
|
||
class Readiness: | ||
@staticmethod | ||
def isPodReady(pod) -> bool: | ||
Utils.checkNotNull(pod, "Pod can't be null.") | ||
|
||
condition = ocp_resources.pod.Pod.Condition.READY | ||
status = ocp_resources.pod.Pod.Condition.Status.TRUE | ||
for cond in pod.get("status", {}).get("conditions", []): | ||
if cond["type"] == condition and cond["status"].casefold() == status.casefold(): | ||
return True | ||
return False | ||
|
||
@staticmethod | ||
def isPodSucceeded(pod) -> bool: | ||
Utils.checkNotNull(pod, "Pod can't be null.") | ||
return pod.status is not None and "Succeeded" == pod.status.phase | ||
|
||
|
||
class Utils: | ||
@staticmethod | ||
def checkNotNull(value, message) -> None: | ||
if value is None: | ||
raise ValueError(message) | ||
|
||
|
||
class TestFrameConstants: | ||
GLOBAL_POLL_INTERVAL_MEDIUM = 10 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from typing import Callable | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this used for? does it just provide additional documentation for tests in e.g. https://github.com/opendatahub-io/opendatahub-tests/pull/94/files#diff-358f1122838e1ac22415822c5a85d419e2d111c64285e488695af1cf6f03d490R35 ? |
||
import typing | ||
|
||
T = typing.TypeVar("T") | ||
StepRType = tuple[str, str] | ||
|
||
|
||
def Desc(value: str) -> str: | ||
return value | ||
|
||
|
||
def Step( | ||
value: str, | ||
expected: str, | ||
) -> StepRType: | ||
return value, expected | ||
|
||
|
||
def SuiteDoc( | ||
description: str, | ||
beforeTestSteps: set[StepRType], | ||
afterTestSteps: set[StepRType], | ||
) -> Callable[[T], T]: | ||
return lambda x: x | ||
|
||
|
||
def Contact( | ||
name: str, | ||
email: str, | ||
) -> tuple[str, str]: | ||
return name, email | ||
|
||
|
||
def TestDoc( | ||
description: str, | ||
contact: str, | ||
steps: set[StepRType], | ||
) -> Callable[[T], T]: | ||
return lambda x: x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the majority of these classes could be avoided if you instead used the wrapper library https://github.com/RedHatQE/openshift-python-wrapper