diff --git a/pyproject.toml b/pyproject.toml index df81d6b..5ab91fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dependencies = [ "pytest-testconfig>=0.2.0", "pytest-jira>=0.3.21", "pygithub>=2.5.0", + "allure-pytest>=2.13.5", ] [project.urls] diff --git a/tests/workbenches/__init__.py b/tests/workbenches/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/workbenches/conftest.py b/tests/workbenches/conftest.py new file mode 100644 index 0000000..4646c1a --- /dev/null +++ b/tests/workbenches/conftest.py @@ -0,0 +1,309 @@ +from __future__ import annotations + +import logging +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: + 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): + 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/" + + LABEL_DASHBOARD = ODH_DOMAIN + "dashboard" + LABEL_ODH_MANAGED = ODH_DOMAIN + "odh-managed" + LABEL_SIDECAR_ISTIO_INJECT = "sidecar.istio.io/inject" + + 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 + + +class OdhConstants: + # private static final Logger LOGGER = LoggerFactory.getLogger(OdhConstants.class); + # private static final Map VALUES = new HashMap<>(); + # // 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: + 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 diff --git a/tests/workbenches/docs.py b/tests/workbenches/docs.py new file mode 100644 index 0000000..fbb85de --- /dev/null +++ b/tests/workbenches/docs.py @@ -0,0 +1,39 @@ +from typing import Callable +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 diff --git a/tests/workbenches/notebook-controller/test_data/notebook.yaml b/tests/workbenches/notebook-controller/test_data/notebook.yaml new file mode 100644 index 0000000..6074cd9 --- /dev/null +++ b/tests/workbenches/notebook-controller/test_data/notebook.yaml @@ -0,0 +1,146 @@ +apiVersion: kubeflow.org/v1 +kind: Notebook +metadata: + annotations: + notebooks.opendatahub.io/inject-oauth: 'true' + opendatahub.io/service-mesh: 'false' + opendatahub.io/accelerator-name: '' + labels: + app: my-workbench + opendatahub.io/dashboard: 'true' + opendatahub.io/odh-managed: 'true' + sidecar.istio.io/inject: 'false' + name: my-workbench + namespace: my-project +spec: + template: + spec: + affinity: {} + containers: + - env: + - name: NOTEBOOK_ARGS + value: |- + --ServerApp.port=8888 + --ServerApp.token='' + --ServerApp.password='' + --ServerApp.base_url=/notebook/my-project/my-workbench + --ServerApp.quit_button=False + --ServerApp.tornado_settings={"user":"odh_user","hub_host":"odh_dashboard_route","hub_prefix":"/projects/my-project"} + - name: JUPYTER_IMAGE + value: notebook_image_placeholder + image: notebook_image_placeholder + imagePullPolicy: Always + livenessProbe: + failureThreshold: 3 + httpGet: + path: /notebook/my-project/my-workbench/api + port: notebook-port + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: my-workbench + ports: + - containerPort: 8888 + name: notebook-port + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /notebook/my-project/my-workbench/api + port: notebook-port + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "1" + memory: 1Gi + volumeMounts: + - mountPath: /opt/app-root/src + name: my-workbench + - mountPath: /dev/shm + name: shm + workingDir: /opt/app-root/src + - args: + - --provider=openshift + - --https-address=:8443 + - --http-address= + - --openshift-service-account=my-workbench + - --cookie-secret-file=/etc/oauth/config/cookie_secret + - --cookie-expire=24h0m0s + - --tls-cert=/etc/tls/private/tls.crt + - --tls-key=/etc/tls/private/tls.key + - --upstream=http://localhost:8888 + - --upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + - --email-domain=* + - --skip-provider-button + - --openshift-sar={"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org","resourceName":"my-workbench","namespace":"$(NAMESPACE)"} + - --logout-url=odh_dashboard_route/projects/my-project?notebookLogout=my-workbench + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: registry.redhat.io/openshift4/ose-oauth-proxy:v4.10 + imagePullPolicy: Always + livenessProbe: + failureThreshold: 3 + httpGet: + path: /oauth/healthz + port: oauth-proxy + scheme: HTTPS + initialDelaySeconds: 30 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: oauth-proxy + ports: + - containerPort: 8443 + name: oauth-proxy + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /oauth/healthz + port: oauth-proxy + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 100m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi + volumeMounts: + - mountPath: /etc/oauth/config + name: oauth-config + - mountPath: /etc/tls/private + name: tls-certificates + enableServiceLinks: false + serviceAccountName: my-workbench + volumes: + - name: my-workbench + persistentVolumeClaim: + claimName: my-workbench + - emptyDir: + medium: Memory + name: shm + - name: oauth-config + secret: + defaultMode: 420 + secretName: my-workbench-oauth-config # pragma: allowlist secret + - name: tls-certificates + secret: + defaultMode: 420 + secretName: my-workbench-tls # pragma: allowlist secret diff --git a/tests/workbenches/notebook-controller/test_spawning.py b/tests/workbenches/notebook-controller/test_spawning.py new file mode 100644 index 0000000..681a18a --- /dev/null +++ b/tests/workbenches/notebook-controller/test_spawning.py @@ -0,0 +1,201 @@ +# +# Copyright OpenDataHub authors. +# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). +# +from __future__ import annotations + +import functools +import io +import logging +import pathlib + +import allure +import ocp_resources.namespace +import ocp_resources.persistent_volume_claim +import ocp_resources.route +import ocp_resources.resource + +import kubernetes.client +from kubernetes.dynamic import DynamicClient + +from tests.workbenches.conftest import OdhAnnotationsLabels, OdhConstants, PodUtils +from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step + + +# @allure.title("Test Authentication") +# @allure.description("Verifies deployments of Notebooks via GitOps approach") +# @allure.tag("NewUI", "Essentials", "Authentication") +# @allure.severity(allure.severity_level.CRITICAL) +# @allure.label("owner", "John Doe") +# @allure.link("https://dev.example.com/", name="Website") +# @allure.issue("AUTH-123") +# @allure.testcase("TMS-456") + + +@SuiteDoc( + description=Desc("Verifies deployments of Notebooks via GitOps approach"), + beforeTestSteps={ + Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"), + Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"), + Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"), + Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"), + Step(value="Deploy DSCI", expected="DSCI is created and ready"), + Step(value="Deploy DSC", expected="DSC is created and ready"), + }, + afterTestSteps={ + Step( + value="Delete ODH operator and all created resources", + expected="Operator is removed and all other resources as well", + ) + }, +) +class TestNotebookST: + @classmethod + @property + @functools.cache + def logger(cls): + return logging.getLogger(cls.__name__) + + DS_PROJECT_NAME: str = "test-notebooks" + + NTB_NAME: str = "test-odh-notebook" + NTB_NAMESPACE: str = "test-odh-notebook" + + @TestDoc( + description=Desc("Create simple Notebook with all needed resources and see if Operator creates it properly"), + contact=Contact(name="Jakub Stejskal", email="jstejska@redhat.com"), + steps={ + Step( + value="Create namespace for Notebook resources with proper name, labels and annotations", + expected="Namespace is created", + ), + Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"), + Step( + value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace", + expected="Notebook resource is created", + ), + Step( + value="Wait for Notebook pods readiness", + expected="Notebook pods are up and running, Notebook is in ready state", + ), + }, + ) + def testCreateSimpleNotebook(self, function_resource_manager, admin_client, unprivileged_client): + with allure.step("Create namespace"): + ns: ocp_resources.namespace.Namespace = ocp_resources.namespace.Namespace( + name=self.NTB_NAMESPACE, + label={OdhAnnotationsLabels.LABEL_DASHBOARD: "true"}, + annotations={OdhAnnotationsLabels.ANNO_SERVICE_MESH: "false"}, + ) + function_resource_manager.createResourceWithoutWait(unprivileged_client, ns) + + with allure.step("Create PersistentVolumeClaim"): + pvc: ocp_resources.persistent_volume_claim.PersistentVolumeClaim = ( + ocp_resources.persistent_volume_claim.PersistentVolumeClaim( + name=self.NTB_NAME, + namespace=self.NTB_NAMESPACE, + label={OdhAnnotationsLabels.LABEL_DASHBOARD: "true"}, + accessmodes="ReadWriteOnce", + size="10Gi", + volume_mode=ocp_resources.persistent_volume_claim.PersistentVolumeClaim.VolumeMode.FILE, + ) + ) + function_resource_manager.createResourceWithoutWait(unprivileged_client, pvc) + + with allure.step("Create Notebook CR"): + # notebookImage: str = NotebookType.getNotebookImage(NotebookType.JUPYTER_MINIMAL_IMAGE, NotebookType.JUPYTER_MINIMAL_2023_2_TAG); + notebookImage: str = "quay.io/modh/odh-minimal-notebook-container@sha256:615af25cfd4f3f2981b173e1a5ab24cb79f268ee72dabbddb6867ee1082eb902" + # Notebook notebook = new NotebookBuilder(NotebookType.loadDefaultNotebook(NTB_NAMESPACE, NTB_NAME, notebookImage)).build(); + notebook = loadDefaultNotebook(unprivileged_client, self.NTB_NAMESPACE, self.NTB_NAME, notebookImage) + function_resource_manager.createResourceWithoutWait(unprivileged_client, notebook) + + with allure.step("Wait for Notebook pod readiness"): + lblSelector: str = f"app={self.NTB_NAME}" + PodUtils.waitForPodsReady(admin_client, self.NTB_NAMESPACE, lblSelector, 1) + + +# +# @BeforeAll +# void deployDataScienceCluster() { +# if (Environment.SKIP_DEPLOY_DSCI_DSC) { +# LOGGER.info("DSCI and DSC deploy is skipped"); +# return; +# } +# // Create DSCI +# DSCInitialization dsci = DscUtils.getBasicDSCI(); +# +# // Create DSC +# DataScienceCluster dsc = new DataScienceClusterBuilder() +# .withNewMetadata() +# .withName(DS_PROJECT_NAME) +# .endMetadata() +# .withNewSpec() +# .withComponents( +# new ComponentsBuilder() +# .withWorkbenches( +# new WorkbenchesBuilder().withManagementState(Workbenches.ManagementState.Managed).build() +# ) +# .withDashboard( +# new DashboardBuilder().withManagementState(Dashboard.ManagementState.Managed).build() +# ) +# .withKserve( +# new KserveBuilder().withManagementState(Kserve.ManagementState.Managed).build() +# ) +# .withKueue( +# new KueueBuilder().withManagementState(Kueue.ManagementState.Managed).build() +# ) +# .withCodeflare( +# new CodeflareBuilder().withManagementState(Codeflare.ManagementState.Managed).build() +# ) +# .withDatasciencepipelines( +# new DatasciencepipelinesBuilder().withManagementState(Datasciencepipelines.ManagementState.Managed).build() +# ) +# .withModelmeshserving( +# new ModelmeshservingBuilder().withManagementState(Modelmeshserving.ManagementState.Managed).build() +# ) +# .withRay( +# new RayBuilder().withManagementState(Ray.ManagementState.Managed).build() +# ) +# .build()) +# .endSpec() +# .build(); +# // Deploy DSCI,DSC +# KubeResourceManager.getInstance().createOrUpdateResourceWithWait(dsci); +# KubeResourceManager.getInstance().createResourceWithWait(dsc); +# } +# } + + +def loadDefaultNotebook(client: DynamicClient, namespace: str, name: str, image: str) -> Notebook: + notebookString = pathlib.Path( + "/Users/jdanek/IdeaProjects/opendatahub-tests/tests/workbenches/notebook-controller/test_data/notebook.yaml" + ).read_text() + notebookString = notebookString.replace("my-project", namespace).replace("my-workbench", name) + # Set new Route url + # routeHost: str = cast(ocp_resources.route.Route, client.resources.get(resource=, name=OdhConstants.DASHBOARD_ROUTE_NAME, namespace = OdhConstants.CONTROLLERS_NAMESPACE)).host + routeHost: str = list( + ocp_resources.route.Route.get( + client=client, name=OdhConstants.DASHBOARD_ROUTE_NAME, namespace=OdhConstants.CONTROLLERS_NAMESPACE + ) + )[0].host + notebookString = notebookString.replace("odh_dashboard_route", "https://" + routeHost) + # Set correct username (see kubectl -v8 auth whoami) + self_subject_review_resource: kubernetes.dynamic.Resource = client.resources.get( + api_version="authentication.k8s.io/v1", kind="SelfSubjectReview" + ) + self_subject_review: kubernetes.dynamic.ResourceInstance = client.create(self_subject_review_resource) + username: str = self_subject_review.status.userInfo.username + notebookString = notebookString.replace("odh_user", username) + # Replace image + notebookString = notebookString.replace("notebook_image_placeholder", image) + + nb = Notebook(yaml_file=io.StringIO(notebookString)) + # notebook_resource: kubernetes.dynamic.Resource = client.resources.get(api_version="kubeflow.org/v1", kind="Notebook") + # notebook: kubernetes.dynamic.ResourceInstance = kubernetes.dynamic.ResourceInstance(client=client, instance=yaml.safe_load(notebookString)) + + return nb + # return ocp_resources.resource.NamespacedResource(client=client, api_group="kubeflow.org", yaml_file=io.StringIO(notebookString)) + + +class Notebook(ocp_resources.resource.NamespacedResource): + api_group: str = "kubeflow.org" diff --git a/uv.lock b/uv.lock index 596f323..704cada 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,32 @@ resolution-markers = [ "python_full_version >= '3.11'", ] +[[package]] +name = "allure-pytest" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "allure-python-commons" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/c3/829a300b72557e9327fa72692b62917dab3ad5bab678962ea84c066d4eef/allure-pytest-2.13.5.tar.gz", hash = "sha256:0ef8e1790c44a988db6b83c4d4f5e91451e2c4c8ea10601dfa88528d23afcf6e", size = 16976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/7b/430830ed7bf1ef078f9e55eb4b19cc059ef5310a20b5bd73eeb99e2c5ff1/allure_pytest-2.13.5-py3-none-any.whl", hash = "sha256:94130bac32964b78058e62cf4b815ad97a5ac82a065e6dd2d43abac2be7640fc", size = 11606 }, +] + +[[package]] +name = "allure-python-commons" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/96/9991a10dcd25b98c8c3e4c7916780f33f13a55a0efbe8c7ea96505271d91/allure-python-commons-2.13.5.tar.gz", hash = "sha256:a232e7955811f988e49a4c1dd6c16cce7e9b81d0ea0422b1e5654d3254e2caf3", size = 12934 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/18/79a66d6adc301281803398f7288583dff8d1d3f2672c07ffab03ee12c7cd/allure_python_commons-2.13.5-py3-none-any.whl", hash = "sha256:8b0e837b6e32d810adec563f49e1d04127a5b6770e0232065b7cb09b9953980d", size = 15715 }, +] + [[package]] name = "anyio" version = "4.8.0" @@ -270,7 +296,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -1089,6 +1115,7 @@ name = "opendatahub-tests" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "allure-pytest" }, { name = "grpcio-reflection" }, { name = "ipython" }, { name = "openshift-python-utilities" }, @@ -1118,6 +1145,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "allure-pytest", specifier = ">=2.13.5" }, { name = "grpcio-reflection" }, { name = "ipython", specifier = ">=8.18.1" }, { name = "openshift-python-utilities", specifier = ">=5.0.71" },