Repository: opendatahub-io/opendatahub-tests
Pull request #94: RHOAIENG-16055: new(tests): test to start a Workbench, by creating the Notebook CR directly
Author: login: jiridanek
Labels: name: Verified
description: Verified pr in Jenkins
name: size/l
name: commented-by-jiridanek
name: commented-by-jstourac
Created: 2025-01-15T17:04:03Z | Merged: 2025-03-23T13:18:36Z

* https://issues.redhat.com/browse/RHOAIENG-15389
* https://issues.redhat.com/browse/RHOAIENG-16055

## Description

This is a port of

* https://github.com/skodjob/odh-e2e/blob/main/src/test/java/io/odh/test/e2e/standard/NotebookST.java#L78-L125

Based on the change in

* https://github.com/opendatahub-io/opendatahub-tests/pull/184

## How Has This Been Tested?

* [x] figure out when/how to use unprivileged k8s client
* [x] test passed with RHOAI 2.19 nightly

## Merge criteria:

- [x] The commits are squashed in a cohesive manner and have meaningful messages.
- [x] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that the changes work



Diff:
diff --git a/tests/conftest.py b/tests/conftest.py
index bef921d54..b4a5a79a5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -336,5 +336,8 @@ def unprivileged_model_namespace(
         request.getfixturevalue(argname="enabled_modelmesh_in_dsc")
         ns_kwargs["model_mesh_enabled"] = True
 
+    if (_dashboard_label := request.param.get("dashboard-label")) is not None:
+        ns_kwargs["add_dashboard_label"] = _dashboard_label
+
     with create_ns(**ns_kwargs) as ns:
         yield ns
diff --git a/tests/workbenches/__init__.py b/tests/workbenches/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/workbenches/conftest.py b/tests/workbenches/conftest.py
new file mode 100644
index 000000000..e64a21f79
--- /dev/null
+++ b/tests/workbenches/conftest.py
@@ -0,0 +1,204 @@
+from __future__ import annotations
+
+from typing import Generator
+
+import pytest
+from pytest_testconfig import config as py_config
+
+
+from tests.workbenches.utils import get_username
+
+from kubernetes.dynamic import DynamicClient
+from kubernetes.dynamic.exceptions import ResourceNotFoundError
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+from utilities.constants import Labels
+from utilities import constants
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
+    request: pytest.FixtureRequest, unprivileged_model_namespace: Namespace, unprivileged_client: DynamicClient
+) -> Generator[PersistentVolumeClaim, None, None]:
+    with PersistentVolumeClaim(
+        client=unprivileged_client,
+        name=request.param["name"],
+        namespace=unprivileged_model_namespace.name,
+        label={constants.Labels.OpenDataHub.DASHBOARD: "true"},
+        accessmodes=PersistentVolumeClaim.AccessMode.RWO,
+        size="10Gi",
+        volume_mode=PersistentVolumeClaim.VolumeMode.FILE,
+    ) as pvc:
+        yield pvc
+
+
+@pytest.fixture(scope="function")
+def minimal_image() -> Generator[str, None, None]:
+    """Provides a full image name of a minimal workbench image"""
+    image_name = "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
+    yield f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{image_name}:{'2024.2'}"
+
+
+@pytest.fixture(scope="function")
+def default_notebook(
+    request: pytest.FixtureRequest,
+    admin_client: DynamicClient,
+    minimal_image: str,
+) -> Generator[Notebook, None, None]:
+    """Returns a new Notebook CR for a given namespace, name, and image"""
+    namespace = request.param["namespace"]
+    name = request.param["name"]
+
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=admin_client, name=route_name, namespace=py_config["applications_namespace"])
+    if not route.exists:
+        raise ResourceNotFoundError(f"Route {route.name} does not exist")
+
+    # Set the correct username
+    username = get_username(dyn_client=admin_client)
+
+    probe_config = {
+        "failureThreshold": 3,
+        "httpGet": {
+            "path": f"/notebook/{namespace}/{name}/api",
+            "port": "notebook-port",
+            "scheme": "HTTP",
+        },
+        "initialDelaySeconds": 10,
+        "periodSeconds": 5,
+        "successThreshold": 1,
+        "timeoutSeconds": 1,
+    }
+
+    notebook = {
+        "apiVersion": "kubeflow.org/v1",
+        "kind": "Notebook",
+        "metadata": {
+            "annotations": {
+                "notebooks.opendatahub.io/inject-oauth": "true",
+                "opendatahub.io/accelerator-name": "",
+                "opendatahub.io/service-mesh": "false",
+            },
+            "labels": {
+                "app": name,
+                Labels.OpenDataHub.DASHBOARD: "true",
+                "opendatahub.io/odh-managed": "true",
+                "sidecar.istio.io/inject": "false",
+            },
+            "name": name,
+            "namespace": namespace,
+        },
+        "spec": {
+            "template": {
+                "spec": {
+                    "affinity": {},
+                    "containers": [
+                        {
+                            "env": [
+                                {
+                                    "name": "NOTEBOOK_ARGS",
+                                    "value": "--ServerApp.port=8888\n"
+                                    "                  "
+                                    "--ServerApp.token=''\n"
+                                    "                  "
+                                    "--ServerApp.password=''\n"
+                                    "                  "
+                                    f"--ServerApp.base_url=/notebook/{namespace}/{name}\n"
+                                    "                  "
+                                    "--ServerApp.quit_button=False\n"
+                                    "                  "
+                                    f'--ServerApp.tornado_settings={{"user":"{username}","hub_host":"https://{route.host}","hub_prefix":"/projects/{namespace}"}}',  # noqa: E501 line too long
+                                },
+                                {"name": "JUPYTER_IMAGE", "value": minimal_image},
+                            ],
+                            "image": minimal_image,
+                            "imagePullPolicy": "Always",
+                            "livenessProbe": probe_config,
+                            "name": name,
+                            "ports": [{"containerPort": 8888, "name": "notebook-port", "protocol": "TCP"}],
+                            "readinessProbe": probe_config,
+                            "resources": {
+                                "limits": {"cpu": "2", "memory": "4Gi"},
+                                "requests": {"cpu": "1", "memory": "1Gi"},
+                            },
+                            "volumeMounts": [
+                                {"mountPath": "/opt/app-root/src", "name": name},
+                                {"mountPath": "/dev/shm", "name": "shm"},
+                            ],
+                            "workingDir": "/opt/app-root/src",
+                        },
+                        {
+                            "args": [
+                                "--provider=openshift",
+                                "--https-address=:8443",
+                                "--http-address=",
+                                f"--openshift-service-account={name}",
+                                "--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",
+                                f'--openshift-sar={{"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org","resourceName":"{name}","namespace":"$(NAMESPACE)"}}',  # noqa: E501 line too long
+                                f"--logout-url=https://{route.host}/projects/{namespace}?notebookLogout={name}",
+                            ],
+                            "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": name,
+                    "volumes": [
+                        {"name": name, "persistentVolumeClaim": {"claimName": name}},
+                        {"emptyDir": {"medium": "Memory"}, "name": "shm"},
+                        {
+                            "name": "oauth-config",
+                            "secret": {"defaultMode": 420, "secretName": f"{name}-oauth-config"},
+                        },
+                        {"name": "tls-certificates", "secret": {"defaultMode": 420, "secretName": f"{name}-tls"}},
+                    ],
+                }
+            }
+        },
+    }
+
+    with Notebook(kind_dict=notebook) as nb:
+        yield nb
diff --git a/tests/workbenches/notebook-controller/test_spawning.py b/tests/workbenches/notebook-controller/test_spawning.py
new file mode 100644
index 000000000..ffd42f51f
--- /dev/null
+++ b/tests/workbenches/notebook-controller/test_spawning.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+import pytest
+
+from timeout_sampler import TimeoutExpiredError
+
+from ocp_resources.pod import Pod
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "unprivileged_model_namespace,users_persistent_volume_claim,default_notebook",
+        [
+            pytest.param(
+                {
+                    "name": "test-odh-notebook",
+                    "dashboard-label": True,
+                },
+                {"name": "test-odh-notebook"},
+                {
+                    "namespace": "test-odh-notebook",
+                    "name": "test-odh-notebook",
+                },
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self,
+        unprivileged_client,
+        unprivileged_model_namespace,
+        users_persistent_volume_claim,
+        default_notebook,
+    ):
+        """
+        Create a simple Notebook CR with all necessary resources and see if the Notebook Operator creates it properly
+        """
+        pods = Pod.get(
+            dyn_client=unprivileged_client,
+            namespace=unprivileged_model_namespace.name,
+            label_selector=f"app={unprivileged_model_namespace.name}",
+        )
+        assert pods, "The expected notebook pods were not found"
+
+        failed_pods = []
+        for pod in pods:
+            try:
+                pod.wait_for_condition(condition=pod.Condition.READY, status=pod.Condition.Status.TRUE)
+            except TimeoutExpiredError:
+                failed_pods.append(pod)
+        assert not failed_pods, f"The following pods failed to get READY when starting the notebook: {failed_pods}"
diff --git a/tests/workbenches/utils.py b/tests/workbenches/utils.py
new file mode 100644
index 000000000..ee1bd4024
--- /dev/null
+++ b/tests/workbenches/utils.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+
+def get_username(dyn_client: DynamicClient) -> str:
+    """Gets the username for the client (see kubectl -v8 auth whoami)"""
+    self_subject_review_resource: Resource = dyn_client.resources.get(
+        api_version="authentication.k8s.io/v1", kind="SelfSubjectReview"
+    )
+    self_subject_review: ResourceInstance = dyn_client.create(self_subject_review_resource)
+    username: str = self_subject_review.status.userInfo.username
+    return username
diff --git a/utilities/constants.py b/utilities/constants.py
index be56df0cf..be94040d3 100644
--- a/utilities/constants.py
+++ b/utilities/constants.py
@@ -2,6 +2,8 @@
 
 from ocp_resources.resource import Resource
 
+OPENDATAHUB_IO: str = "opendatahub.io"
+
 
 class KServeDeploymentType:
     SERVERLESS: str = "Serverless"
@@ -121,10 +123,11 @@ class KserveIo:
         DEPLOYMENT_MODE: str = "serving.kserve.io/deploymentMode"
 
     class KserveAuth:
-        SECURITY: str = "security.opendatahub.io/enable-auth"
+        SECURITY: str = f"security.{OPENDATAHUB_IO}/enable-auth"
 
     class OpenDataHubIo:
-        MANAGED: str = "opendatahub.io/managed"
+        MANAGED: str = f"{OPENDATAHUB_IO}/managed"
+        SERVICE_MESH: str = f"{OPENDATAHUB_IO}/service-mesh"
 
 
 class StorageClassName:
@@ -154,10 +157,13 @@ class ConditionType:
 
 class Labels:
     class OpenDataHub:
-        DASHBOARD: str = "opendatahub.io/dashboard"
+        DASHBOARD: str = f"{OPENDATAHUB_IO}/dashboard"
 
     class KserveAuth:
-        SECURITY: str = "security.opendatahub.io/enable-auth"
+        SECURITY: str = f"security.{OPENDATAHUB_IO}/enable-auth"
+
+    class Notebook:
+        INJECT_OAUTH: str = f"notebooks.{OPENDATAHUB_IO}/inject-oauth"
 
 
 class Timeout:
@@ -173,6 +179,7 @@ class Timeout:
 MODELMESH_SERVING: str = "modelmesh-serving"
 ISTIO_CA_BUNDLE_FILENAME: str = "istio_knative.crt"
 OPENSHIFT_CA_BUNDLE_FILENAME: str = "openshift_ca.crt"
+INTERNAL_IMAGE_REGISTRY_PATH: str = "image-registry.openshift-image-registry.svc:5000"
 
 vLLM_CONFIG: dict[str, dict[str, Any]] = {
     "port_configurations": {


Reviews:
user: login: rnetser
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-19T08:18:36Z
user: login: lugi0
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-20T08:54:20Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T08:48:42Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T08:49:00Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T08:49:31Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T08:50:33Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T08:52:27Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T08:52:57Z
user: login: lugi0
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T11:26:44Z
user: login: adolfo-ab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T15:15:25Z
user: login: rnetser
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T15:02:04Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T09:19:02Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T09:21:23Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T09:39:01Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T09:46:06Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T09:46:40Z
user: login: mwaykole
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T10:42:33Z
user: login: mwaykole
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T10:43:26Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T11:50:30Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T11:56:35Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T11:56:41Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T11:57:21Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T11:59:04Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T11:59:30Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T11:59:41Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:17:42Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:26:26Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:27:34Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:28:37Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:31:47Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:33:13Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:33:37Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:33:48Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T12:33:56Z
user: login: lugi0
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T16:35:33Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T09:53:43Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T09:54:15Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T09:54:22Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T09:54:31Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T09:56:59Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T09:57:11Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T09:58:00Z
user: login: myakove
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-25T10:35:49Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T10:40:59Z
user: login: myakove
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-25T10:44:53Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:45:45Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:45:52Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:46:39Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:47:13Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:47:22Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:51:13Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T11:42:11Z
user: login: myakove
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T11:57:58Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T12:05:24Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T12:06:00Z
user: login: rnetser
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T14:54:52Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:20:15Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:20:22Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:21:00Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:21:13Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:21:21Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:21:51Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:22:00Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:22:30Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:22:45Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:44:02Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:50:58Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T19:52:15Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:31:44Z
user: login: jstourac
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T13:57:31Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T15:26:13Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T15:28:39Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T15:30:13Z
user: login: jstourac
body: Thank you, Jiri.

/lgtm

@rnetser what are the next steps here? Can this be merged in now?
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T13:56:55Z
user: login: rnetser
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T20:00:46Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:11:21Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:12:46Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:14:33Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:16:02Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:20:18Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:21:57Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:32:59Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T20:55:10Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T12:19:46Z
user: login: lugi0
body: bit of a nitpick, otherwise it looks good to me. I see #184 pushing some of the bits here, so not sure if this needs a rebase before merging.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:35:13Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T17:53:30Z
user: login: rnetser
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:48:18Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T14:55:28Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T14:56:03Z
user: login: rnetser
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:35:17Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:48:32Z
user: login: mwaykole
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:56:20Z
user: login: jiridanek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T16:01:07Z
user: login: adolfo-ab
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T10:29:37Z
user: login: lugi0
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:12:12Z
user: login: rnetser
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T09:58:30Z

Review comments:
diff_hunk: @@ -0,0 +1,309 @@
+from __future__ import annotations
+
+import logging
path: tests/workbenches/conftest.py
user: login: rnetser
body: please use simple_logger
created_at: 2025-01-19T07:57:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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:
path: tests/workbenches/conftest.py
user: login: rnetser
body: please use ocpenshift python wrapper to CRUD resources
created_at: 2025-01-19T08:00:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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/"
path: tests/workbenches/conftest.py
user: login: rnetser
body: used in other places as well, please place under https://github.com/opendatahub-io/opendatahub-tests/blob/main/utilities/constants.py and re-use
created_at: 2025-01-19T08:02:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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"
path: tests/workbenches/conftest.py
user: login: rnetser
body: ditto
created_at: 2025-01-19T08:03:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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):
path: tests/workbenches/conftest.py
user: login: rnetser
body: please use python naming conventions 
created_at: 2025-01-19T08:04:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: tests/workbenches/conftest.py
user: login: rnetser
body: please use py_config["distrobution"] where needed 
created_at: 2025-01-19T08:05:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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<String, String> VALUES = new HashMap<>();
path: tests/workbenches/conftest.py
user: login: rnetser
body: leftovers?
created_at: 2025-01-19T08:05:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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<String, String> 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:
path: tests/workbenches/conftest.py
user: login: rnetser
body: please use openshift python wrapper , e.g https://github.com/RedHatQE/openshift-python-wrapper/blob/main/ocp_resources/resource.py#L769

created_at: 2025-01-19T08:07:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: under conftest place fixtures; functions and classes should be under `utils.py`  in the relevant dir
created_at: 2025-01-19T08:09:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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):
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: please use simple_logger
created_at: 2025-01-19T08:10:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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):
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: all setup steps (e.g namespace, pvc) should be done in fixtures
please check existing fixtures and re-use
created_at: 2025-01-19T08:12:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: functions should go under `utils.py` in the relevant dir
created_at: 2025-01-19T08:13:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+apiVersion: kubeflow.org/v1
+kind: Notebook
path: tests/workbenches/notebook-controller/test_data/notebook.yaml
user: login: rnetser
body: is there a template for this in the cluster?
created_at: 2025-01-19T08:18:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: lugi0
body: Can this be fetched dinamically from the cluster?
created_at: 2025-01-20T08:47:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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()
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: lugi0
body: Pretty sure this won't work :)
created_at: 2025-01-20T08:47:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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",
+        )
+    },
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: lugi0
body: I don't think this has been functionally implemented but FWIW we don't plan to remove the operator after the test run
created_at: 2025-01-20T08:49:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+from typing import Callable
path: tests/workbenches/docs.py
user: login: lugi0
body: 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 ? 
IMHO if this is implemented we should use it across the repo to keep consistency, so this should go under a higher level directory, but it should probably be discussed beforehand. Can you attend the shift left wg meeting to do so?
created_at: 2025-01-20T08:52:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,309 @@
+from __future__ import annotations
path: tests/workbenches/conftest.py
user: login: lugi0
body: I think the majority of these classes could be avoided if you instead used the wrapper library https://github.com/RedHatQE/openshift-python-wrapper
created_at: 2025-01-20T08:54:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+apiVersion: kubeflow.org/v1
+kind: Notebook
path: tests/workbenches/notebook-controller/test_data/notebook.yaml
user: login: jiridanek
body: no, we don't have template for notebooks, the only template we have is hardcoded in odh-dashboard

https://github.com/opendatahub-io/odh-dashboard/blob/eabd833b5f4b0508fe5b06235f4f670fc7e9a0db/backend/src/utils/notebookUtils.ts#L260-L280
created_at: 2025-02-04T08:48:42Z
author_association: MEMBER
diff_hunk: @@ -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"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: yes, I changed the approach, thanks
created_at: 2025-02-04T08:49:00Z
author_association: MEMBER
diff_hunk: @@ -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()
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: ofc, fixed now
created_at: 2025-02-04T08:49:31Z
author_association: MEMBER
diff_hunk: @@ -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",
+        )
+    },
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: fixed the doc comment, I never tried to implement operator removal here
created_at: 2025-02-04T08:50:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+from typing import Callable
path: tests/workbenches/docs.py
user: login: jiridanek
body: I'm not sure how useful this docs is going to be. Yes, it is only for structured comments. The Java version was able to render markdown docs from this, but I did not implement this in Python here.

IMO it's fine for now to only have this in the workbenches tests, and we (in workbenches team) need to decide if this is what we want to promote, or if we decide to delete this eventually.
created_at: 2025-02-04T08:52:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,309 @@
+from __future__ import annotations
path: tests/workbenches/conftest.py
user: login: jiridanek
body: used ocp_resources more, thanks
created_at: 2025-02-04T08:52:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+from typing import Callable
path: tests/workbenches/docs.py
user: login: lugi0
body: I would suggest using docstrings in general rather than trying to adapt to Java practices in this case: https://peps.python.org/pep-0257/
We haven't settled on a specific style and we're not yet rendering the documentation, but this seems to be the best bet to standardize across the repo
created_at: 2025-02-07T10:56:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from ocp_resources.resource import NamespacedResource
+
+
+class Notebook(NamespacedResource):
+    api_group: str = "kubeflow.org"
path: tests/workbenches/resources.py
user: login: lugi0
body: IMHO this should be a class in ocp_resources itself
created_at: 2025-02-07T11:21:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
+import time
+import traceback
+from contextlib import contextmanager
+from typing import Any
+
+from typing import Callable, Generator
+
+import kubernetes.dynamic
+from kubernetes.dynamic import DynamicClient, ResourceField
+
+import ocp_resources.pod
+import ocp_resources.resource
+
+
+class TestFrameConstants:
+    GLOBAL_POLL_INTERVAL_MEDIUM = 10
+
+
+class OcpResourceManager:
path: tests/workbenches/utils.py
user: login: lugi0
body: What is the specific goal you have with this class? if you use resources as classes from ocp_resources you will have their lifecycle handled by the wrapper library directly
created_at: 2025-02-07T11:23:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
+import time
+import traceback
+from contextlib import contextmanager
+from typing import Any
+
+from typing import Callable, Generator
+
+import kubernetes.dynamic
+from kubernetes.dynamic import DynamicClient, ResourceField
+
+import ocp_resources.pod
+import ocp_resources.resource
+
+
+class TestFrameConstants:
+    GLOBAL_POLL_INTERVAL_MEDIUM = 10
+
+
+class OcpResourceManager:
+    def __init__(self) -> None:
+        self.resources: list[ocp_resources.resource.Resource] = []
+
+    def enter(self, resource: ocp_resources.resource.Resource) -> None:
+        resource.__enter__()
+        self.resources.append(resource)
+
+    def __enter__(self) -> OcpResourceManager:
+        return self
+
+    def __exit__(self, *args: tuple[Any, ...], **kwargs: dict[str, Any]) -> None:
+        exceptions = []
+        for resource in reversed(self.resources):
+            try:
+                resource.__exit__(*args, **kwargs)
+            except Exception as e:
+                exceptions.append(e)
+        if exceptions:
+            raise RuntimeError(f"Exceptions during cleanup: {exceptions}")
+
+
+class PodUtils:
path: tests/workbenches/utils.py
user: login: lugi0
body: Same as above, if you use the Pod class you can already handle this without having to reimplement these checks
created_at: 2025-02-07T11:23:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,7 @@ def create_ns(
     teardown: bool = True,
     delete_timeout: int = 4 * 60,
     labels: Optional[dict[str, str]] = None,
+    annotations: Optional[dict[str, str]] = None,
path: utilities/infra.py
user: login: lugi0
body: I think we want to move away from option and use the `xx | yy` syntax by importing from future, see https://docs.python.org/3/library/stdtypes.html#types-union
created_at: 2025-02-07T11:26:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+apiVersion: kubeflow.org/v1
+kind: Notebook
path: tests/workbenches/notebook-controller/test_data/notebook.yaml
user: login: adolfo-ab
body: Why do we need the yaml? Can't we pass the arguments like any other resource?
created_at: 2025-02-07T15:10:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return registry_path + "/" + controllers_namespace + "/" + image_dict[image_name] + ":" + image_tag
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: adolfo-ab
body: can we use fstring for better readability?
created_at: 2025-02-07T15:11:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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",
+            ),
+        },
+    )
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: adolfo-ab
body: I find this excessively verbose, but I'm open to use this format if we all agree.
created_at: 2025-02-07T15:13:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return registry_path + "/" + controllers_namespace + "/" + image_dict[image_name] + ":" + image_tag
+
+
+def _load_default_notebook(client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_host: str = list(
+        Route.get(client=client, name=constants.Dashboard.route_name, namespace=py_config["applications_namespace"])
+    )[0].host
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route_host)
+    # Set the correct username
+    username = _get_username(client=client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    nb = Notebook(yaml_file=io.StringIO(notebook_string))
+
+    return nb
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: adolfo-ab
body: no need to have nb variable, just return Notebook(yaml_file=io.StringIO(notebook_string))
created_at: 2025-02-07T15:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from ocp_resources.resource import NamespacedResource
+
+
+class Notebook(NamespacedResource):
+    api_group: str = "kubeflow.org"
path: tests/workbenches/resources.py
user: login: adolfo-ab
body: +1
created_at: 2025-02-07T15:14:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
+    request: pytest.FixtureRequest, unprivileged_client: DynamicClient
+) -> Generator[Namespace, None, None]:
+    with create_ns(
+        unprivileged_client=unprivileged_client,
+        name=request.param["name"],
+        labels={constants.Dashboard.label: "true"},
+        annotations={constants.ServiceMesh.annotation_inject: "false"},
+    ) as ns:
+        yield ns
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
+    request: pytest.FixtureRequest, unprivileged_client: DynamicClient
+) -> Generator[PersistentVolumeClaim, None, None]:
+    with PersistentVolumeClaim(
+        client=unprivileged_client,
+        name=request.param["name"],
+        namespace=request.param["namespace"],
+        label={constants.Dashboard.label: "true"},
+        accessmodes="ReadWriteOnce",
path: tests/workbenches/conftest.py
user: login: rnetser
body: ```suggestion
        accessmodes=PersistentVolumeClaim.AccessMode.RWO,
```
created_at: 2025-02-10T14:46:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: please see @lugi0 comment above on dosctrings
created_at: 2025-02-10T14:48:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: iiuc all resources should be created together, so please use 1  set of parameters,
e,g
```
    @pytest.mark.parametrize(
        "users_namespace, users_persistent_volume_claim",
        [
            pytest.param(
                {"name": NTB_NAMESPACE},
                {"name": "xxx"},
            )
        ],
        indirect=True,
    )
```

created_at: 2025-02-10T14:51:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
+    request: pytest.FixtureRequest, unprivileged_client: DynamicClient
+) -> Generator[Namespace, None, None]:
+    with create_ns(
+        unprivileged_client=unprivileged_client,
+        name=request.param["name"],
+        labels={constants.Dashboard.label: "true"},
+        annotations={constants.ServiceMesh.annotation_inject: "false"},
+    ) as ns:
+        yield ns
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
path: tests/workbenches/conftest.py
user: login: rnetser
body: call `users_namespace` and then you can use it's attributes. e.g `users_namespace.name` to set the namespace
created_at: 2025-02-10T14:52:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: can we get it from the cluster?
created_at: 2025-02-10T14:55:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: please place functions under `utils.py` file to keep the tests file only for tests
created_at: 2025-02-10T14:55:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return registry_path + "/" + controllers_namespace + "/" + image_dict[image_name] + ":" + image_tag
+
+
+def _load_default_notebook(client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_host: str = list(
+        Route.get(client=client, name=constants.Dashboard.route_name, namespace=py_config["applications_namespace"])
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: i think that `client=client` should be `dyn_client=....` 
created_at: 2025-02-10T14:56:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
path: tests/workbenches/utils.py
user: login: rnetser
body: please use `get_logger` from simple_logger
created_at: 2025-02-10T14:58:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
+import time
+import traceback
+from contextlib import contextmanager
+from typing import Any
+
+from typing import Callable, Generator
+
+import kubernetes.dynamic
+from kubernetes.dynamic import DynamicClient, ResourceField
+
+import ocp_resources.pod
path: tests/workbenches/utils.py
user: login: rnetser
body: when possible, please use explicit imports (i.e of only what is needed)
created_at: 2025-02-10T14:58:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,6 +112,19 @@ class StorageClassName:
     NFS: str = "nfs"
 
 
+class Dashboard:
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    label = OPENDATAHUB_IO + "/dashboard"
path: utilities/constants.py
user: login: rnetser
body: arg name should represent what it contains; please place under labels class 
created_at: 2025-02-10T15:01:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,7 @@ def create_ns(
     teardown: bool = True,
     delete_timeout: int = 4 * 60,
     labels: Optional[dict[str, str]] = None,
+    annotations: Optional[dict[str, str]] = None,
path: utilities/infra.py
user: login: jiridanek
body: done
created_at: 2025-02-24T09:19:02Z
author_association: MEMBER
diff_hunk: @@ -109,6 +112,19 @@ class StorageClassName:
     NFS: str = "nfs"
 
 
+class Dashboard:
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    label = OPENDATAHUB_IO + "/dashboard"
path: utilities/constants.py
user: login: jiridanek
body: done
created_at: 2025-02-24T09:21:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
+import time
+import traceback
+from contextlib import contextmanager
+from typing import Any
+
+from typing import Callable, Generator
+
+import kubernetes.dynamic
+from kubernetes.dynamic import DynamicClient, ResourceField
+
+import ocp_resources.pod
path: tests/workbenches/utils.py
user: login: jiridanek
body: from before I'm used to import packages, not individual names, as described for example in https://google.github.io/styleguide/pyguide.html#22-imports

ok, fixed
created_at: 2025-02-24T09:39:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+from typing import Callable
path: tests/workbenches/docs.py
user: login: jiridanek
body: ok then
created_at: 2025-02-24T09:46:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
+    request: pytest.FixtureRequest, unprivileged_client: DynamicClient
+) -> Generator[Namespace, None, None]:
+    with create_ns(
+        unprivileged_client=unprivileged_client,
+        name=request.param["name"],
+        labels={constants.Dashboard.label: "true"},
+        annotations={constants.ServiceMesh.annotation_inject: "false"},
+    ) as ns:
+        yield ns
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
+    request: pytest.FixtureRequest, unprivileged_client: DynamicClient
+) -> Generator[PersistentVolumeClaim, None, None]:
+    with PersistentVolumeClaim(
+        client=unprivileged_client,
+        name=request.param["name"],
+        namespace=request.param["namespace"],
+        label={constants.Dashboard.label: "true"},
+        accessmodes="ReadWriteOnce",
path: tests/workbenches/conftest.py
user: login: jiridanek
body: done
created_at: 2025-02-24T09:46:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,136 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.pod import Pod
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step
+
+
+class TestNotebookST:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: mwaykole
body: what is ST?
created_at: 2025-02-24T10:42:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,136 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.pod import Pod
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step
+
+
+class TestNotebookST:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+            notebook = _load_default_notebook(
+                client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(client=unprivileged_client, namespace=self.NTB_NAMESPACE, label_selector=f"app={self.NTB_NAME}")
+                for pod in pods:
+                    pod.wait_for_condition(condition="Ready", status="True")
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: mwaykole
body: This  can be added in constant files
created_at: 2025-02-24T10:43:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,136 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.pod import Pod
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step
+
+
+class TestNotebookST:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: I don't rightly know, that's how it was called in https://github.com/skodjob/odh-e2e/blob/main/src/test/java/io/odh/test/e2e/standard/NotebookST.java

will remove that suffix
created_at: 2025-02-24T11:50:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,136 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.pod import Pod
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step
+
+
+class TestNotebookST:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-24T11:56:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,136 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.pod import Pod
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step
+
+
+class TestNotebookST:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+            notebook = _load_default_notebook(
+                client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(client=unprivileged_client, namespace=self.NTB_NAMESPACE, label_selector=f"app={self.NTB_NAME}")
+                for pod in pods:
+                    pod.wait_for_condition(condition="Ready", status="True")
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-24T11:56:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,11 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from ocp_resources.resource import NamespacedResource
+
+
+class Notebook(NamespacedResource):
+    api_group: str = "kubeflow.org"
path: tests/workbenches/resources.py
user: login: jiridanek
body: allright, maybe if I start contributing to ocp_resources, I can try to do the work
created_at: 2025-02-24T11:57:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return registry_path + "/" + controllers_namespace + "/" + image_dict[image_name] + ":" + image_tag
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-24T11:59:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
+import time
+import traceback
+from contextlib import contextmanager
+from typing import Any
+
+from typing import Callable, Generator
+
+import kubernetes.dynamic
+from kubernetes.dynamic import DynamicClient, ResourceField
+
+import ocp_resources.pod
+import ocp_resources.resource
+
+
+class TestFrameConstants:
+    GLOBAL_POLL_INTERVAL_MEDIUM = 10
+
+
+class OcpResourceManager:
+    def __init__(self) -> None:
+        self.resources: list[ocp_resources.resource.Resource] = []
+
+    def enter(self, resource: ocp_resources.resource.Resource) -> None:
+        resource.__enter__()
+        self.resources.append(resource)
+
+    def __enter__(self) -> OcpResourceManager:
+        return self
+
+    def __exit__(self, *args: tuple[Any, ...], **kwargs: dict[str, Any]) -> None:
+        exceptions = []
+        for resource in reversed(self.resources):
+            try:
+                resource.__exit__(*args, **kwargs)
+            except Exception as e:
+                exceptions.append(e)
+        if exceptions:
+            raise RuntimeError(f"Exceptions during cleanup: {exceptions}")
+
+
+class PodUtils:
path: tests/workbenches/utils.py
user: login: jiridanek
body: done
created_at: 2025-02-24T11:59:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
+import time
+import traceback
+from contextlib import contextmanager
+from typing import Any
+
+from typing import Callable, Generator
+
+import kubernetes.dynamic
+from kubernetes.dynamic import DynamicClient, ResourceField
+
+import ocp_resources.pod
+import ocp_resources.resource
+
+
+class TestFrameConstants:
+    GLOBAL_POLL_INTERVAL_MEDIUM = 10
+
+
+class OcpResourceManager:
path: tests/workbenches/utils.py
user: login: jiridanek
body: removed OcpResourceManager
created_at: 2025-02-24T11:59:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+apiVersion: kubeflow.org/v1
+kind: Notebook
path: tests/workbenches/notebook-controller/test_data/notebook.yaml
user: login: jiridanek
body: i like it this way, see no reason to rewrite it
created_at: 2025-02-24T12:17:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-24T12:26:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return registry_path + "/" + controllers_namespace + "/" + image_dict[image_name] + ":" + image_tag
+
+
+def _load_default_notebook(client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_host: str = list(
+        Route.get(client=client, name=constants.Dashboard.route_name, namespace=py_config["applications_namespace"])
+    )[0].host
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route_host)
+    # Set the correct username
+    username = _get_username(client=client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    nb = Notebook(yaml_file=io.StringIO(notebook_string))
+
+    return nb
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: it's convenient to place breakpoint, if I ever wanted to do that, and look what's in `nb`

fixed now
created_at: 2025-02-24T12:27:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,221 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import logging
path: tests/workbenches/utils.py
user: login: jiridanek
body: fixed
created_at: 2025-02-24T12:28:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return registry_path + "/" + controllers_namespace + "/" + image_dict[image_name] + ":" + image_tag
+
+
+def _load_default_notebook(client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_host: str = list(
+        Route.get(client=client, name=constants.Dashboard.route_name, namespace=py_config["applications_namespace"])
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-24T12:31:47Z
author_association: MEMBER
diff_hunk: @@ -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):
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-24T12:33:12Z
author_association: MEMBER
diff_hunk: @@ -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):
path: tests/workbenches/conftest.py
user: login: jiridanek
body: done
created_at: 2025-02-24T12:33:37Z
author_association: MEMBER
diff_hunk: @@ -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
path: tests/workbenches/conftest.py
user: login: jiridanek
body: done
created_at: 2025-02-24T12:33:47Z
author_association: MEMBER
diff_hunk: @@ -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<String, String> VALUES = new HashMap<>();
path: tests/workbenches/conftest.py
user: login: jiridanek
body: removed
created_at: 2025-02-24T12:33:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,11 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from ocp_resources.resource import NamespacedResource
+
+
+class Notebook(NamespacedResource):
+    api_group: str = "kubeflow.org"
path: tests/workbenches/resources.py
user: login: lugi0
body: There is a tool that will auto-generate the class for ocp_resources, https://github.com/RedHatQE/openshift-python-wrapper/blob/main/class_generator/README.md
If you need any help with it feel free to ask any of us!
created_at: 2025-02-24T16:35:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from ocp_resources.resource import NamespacedResource
+
+
+class Notebook(NamespacedResource):
+    api_group: str = "kubeflow.org"
path: tests/workbenches/resources.py
user: login: jiridanek
body: * https://github.com/RedHatQE/openshift-python-wrapper/pull/2331
created_at: 2025-02-25T09:53:42Z
author_association: MEMBER
diff_hunk: @@ -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:
path: tests/workbenches/conftest.py
user: login: jiridanek
body: done
created_at: 2025-02-25T09:54:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,309 @@
+from __future__ import annotations
+
+import logging
path: tests/workbenches/conftest.py
user: login: jiridanek
body: fixed
created_at: 2025-02-25T09:54:22Z
author_association: MEMBER
diff_hunk: @@ -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:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-25T09:54:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-25T09:56:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
+    request: pytest.FixtureRequest, unprivileged_client: DynamicClient
+) -> Generator[Namespace, None, None]:
+    with create_ns(
+        unprivileged_client=unprivileged_client,
+        name=request.param["name"],
+        labels={constants.Dashboard.label: "true"},
+        annotations={constants.ServiceMesh.annotation_inject: "false"},
+    ) as ns:
+        yield ns
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
path: tests/workbenches/conftest.py
user: login: jiridanek
body: done
created_at: 2025-02-25T09:57:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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 test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        with OcpResourceManager() as test_frame:
+            with step("Create Notebook CR"):
+                notebook_image: str = _get_notebook_image("jupyter-minimal-notebook", "2024.2")
+                notebook = _load_default_notebook(
+                    client=unprivileged_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+                )
+                test_frame.enter(resource=notebook)
+
+            with step("Wait for Notebook pod readiness"):
+                PodUtils.wait_for_pods_ready(
+                    client=admin_client,
+                    namespace_name=self.NTB_NAMESPACE,
+                    label_selector=f"app={self.NTB_NAME}",
+                    expect_pods_count=1,
+                )
+
+
+def _get_notebook_image(image_name: str, image_tag: str) -> str:
+    registry_path = "image-registry.openshift-image-registry.svc:5000"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: ended up following advice from @mwaykole and defined this as a constant
* https://github.com/opendatahub-io/opendatahub-tests/pull/94#pullrequestreview-2636653194
created_at: 2025-02-25T09:58:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,81 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image("jupyter-minimal-notebook", "2024.2")
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: myakove
body: `notebook_image: str` there is no need to declare type here when you call typed function `get_notebook_image`

Nit: It's better to call functions with `arg=value`, if for some reason `get_notebook_image` need to be refactored you will not be bound to positional arguments.
created_at: 2025-02-25T10:19:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,81 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image("jupyter-minimal-notebook", "2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: myakove
body: `pods` can be empty list and your test will pass without test anything, please assert if not `pods`

created_at: 2025-02-25T10:22:01Z
author_association: NONE
diff_hunk: @@ -0,0 +1,81 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image("jupyter-minimal-notebook", "2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(
+                    dyn_client=unprivileged_client, namespace=self.NTB_NAMESPACE, label_selector=f"app={self.NTB_NAME}"
+                )
+                for pod in pods:
+                    pod.wait_for_condition(condition="Ready", status="True")
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: myakove
body: All resources have `condition` and `status` please use them.
```
pod.wait_for_condition(condition=pod.Condition.READY, status=pod.Condition.Status.TRUE)
```
created_at: 2025-02-25T10:24:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
path: tests/workbenches/utils.py
user: login: myakove
body: Simple way to do this will be:
```
image_name = "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config["applications_namespace"]}/{image_name}:{image_tag}"
```

And you can drop `image_name` from the function args
created_at: 2025-02-25T10:28:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route_host: str = list(
+        Route.get(dyn_client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    )[0].host
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route_host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(yaml_file=io.StringIO(notebook_string))
path: tests/workbenches/utils.py
user: login: myakove
body: Resource accept `kind_dict`, it's better to work with dict then with string.
You can load the YAML file with `yaml`, change the needed values inside the dict and use it to create your resource.
 
created_at: 2025-02-25T10:30:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route_host: str = list(
+        Route.get(dyn_client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    )[0].host
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route_host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(yaml_file=io.StringIO(notebook_string))
path: tests/workbenches/utils.py
user: login: jiridanek
body: At that point I can just as well follow other recommendations here to have the file content be a python literal in code...

I guess I'll do that.
created_at: 2025-02-25T10:40:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route_host: str = list(
path: tests/workbenches/utils.py
user: login: myakove
body: For better error handeling I think you should do:
```
route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
if not route.exists:
     assert .......
```

with the current code you will get `IndexError: list index out of range` which say nothing.

created_at: 2025-02-25T10:44:53Z
author_association: NONE
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
path: tests/workbenches/utils.py
user: login: jiridanek
body: I'm not bothered by the extra argument, and i think that at callsite

```
notebook_image: str = get_notebook_image(image_name="jupyter-minimal-notebook", image_tag="2024.2")
```

makes more sense than if image_name parameter was omitted.

I could (eventually) have enum for the possible image names...
created_at: 2025-02-25T11:45:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,81 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image("jupyter-minimal-notebook", "2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-25T11:45:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,81 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image("jupyter-minimal-notebook", "2024.2")
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: well, https://github.com/RedHatQE/openshift-python-wrapper/pull/2331/files#diff-326458d10fbb8ff9e3b95dfec81f67dc7349f85a51d7539fa6c985072e4f9ab3R348-R351 has the types explicitly there too...

changed to use named parameter
created_at: 2025-02-25T11:46:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route_host: str = list(
+        Route.get(dyn_client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    )[0].host
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route_host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(yaml_file=io.StringIO(notebook_string))
path: tests/workbenches/utils.py
user: login: jiridanek
body: went with the kind_dict, but kept using the yaml text file for the initial data
created_at: 2025-02-25T11:47:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route_host: str = list(
path: tests/workbenches/utils.py
user: login: jiridanek
body: done
created_at: 2025-02-25T11:47:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,81 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image("jupyter-minimal-notebook", "2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(
+                    dyn_client=unprivileged_client, namespace=self.NTB_NAMESPACE, label_selector=f"app={self.NTB_NAME}"
+                )
+                for pod in pods:
+                    pod.wait_for_condition(condition="Ready", status="True")
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-25T11:51:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route_host: str = list(
+        Route.get(dyn_client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    )[0].host
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route_host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(yaml_file=io.StringIO(notebook_string))
path: tests/workbenches/utils.py
user: login: jiridanek
body: and kept using string replacement, because modifying the dict selectively is way more work; I need to also update values inside long strings such as container args in the pod
created_at: 2025-02-26T11:42:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image(image_name="jupyter-minimal-notebook", image_tag="2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(
+                    dyn_client=unprivileged_client, namespace=self.NTB_NAMESPACE, label_selector=f"app={self.NTB_NAME}"
+                )
+                assert pods, "The expected notebook pods were not found"
+                for pod in pods:
+                    pod.wait_for_condition(condition=pod.Condition.READY, status=pod.Condition.Status.TRUE)
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: myakove
body: right now you will fail of the first pod in `wait_for_condition`, depends on what you want to test maybe it's better to collect the failed pods and assert once with all of them.

created_at: 2025-02-26T11:48:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
path: tests/workbenches/utils.py
user: login: myakove
body: Up to you, I would remove this function and just set the path in the test where it's needed. (only used in one place) and introduce more complicity without any reason.
 
created_at: 2025-02-26T11:51:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
path: tests/workbenches/utils.py
user: login: myakove
body: I think it's better to load the YAML here and work with dict which is python object and not with strings.

created_at: 2025-02-26T11:52:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image(image_name="jupyter-minimal-notebook", image_tag="2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
+            )
+
+        with step("Wait for Notebook pod readiness"):
+            with notebook:
+                pods = Pod.get(
+                    dyn_client=unprivileged_client, namespace=self.NTB_NAMESPACE, label_selector=f"app={self.NTB_NAME}"
+                )
+                assert pods, "The expected notebook pods were not found"
+                for pod in pods:
+                    pod.wait_for_condition(condition=pod.Condition.READY, status=pod.Condition.Status.TRUE)
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: thanks for the note; failing fast is what I want
created_at: 2025-02-26T12:05:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
path: tests/workbenches/utils.py
user: login: jiridanek
body: ack, will see if I can get approvals from rhoai people without making the change; personally I don't want to make it
created_at: 2025-02-26T12:06:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
path: tests/workbenches/conftest.py
user: login: rnetser
body: you can use `unprivileged_model_namespace`  https://github.com/opendatahub-io/opendatahub-tests/blob/main/tests/model_serving/model_server/authentication/conftest.py#L334 fixture; move it to upper conftest.
you can also add to `create_ns` an new bool arg - `add_dashboard_label` and apply the logic there
created_at: 2025-02-27T14:34:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: we currently do not have license info in our files; any reason to add it?
created_at: 2025-02-27T14:36:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: this is not done in the tests so i am not sure why we need to include it here.
you can call `skip_if_no_deployed_openshift_serverless` and `skip_if_no_deployed_openshift_service_mesh` to skip the tests if these are not deployed or create new fixtures to fail if these are mandaotry
created_at: 2025-02-27T14:39:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: can you please explain what is the value of using `with step..`?
created_at: 2025-02-27T14:43:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: each step should either be a fixture (i.e setup for a test) or a test by itself. tests should verify one thing
created_at: 2025-02-27T14:44:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image(image_name="jupyter-minimal-notebook", image_tag="2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: ```suggestion
                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=users_namespace.name, image=notebook_image
```
created_at: 2025-02-27T14:45:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: no need to set these; pass to the fixtures and then use `users_namespace.name` for example
created_at: 2025-02-27T14:46:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
path: tests/workbenches/utils.py
user: login: rnetser
body: +1 to that; used once and should be set where used 
created_at: 2025-02-27T14:49:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
path: tests/workbenches/utils.py
user: login: rnetser
body: please work with a dict; it is more readable and pythonic
created_at: 2025-02-27T14:50:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route.host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(kind_dict=yaml.safe_load(notebook_string))
+
+
+def get_username(dyn_client: DynamicClient) -> str:
+    """Gets the username for the client (see kubectl -v8 auth whoami)"""
+    self_subject_review_resource: Resource = dyn_client.resources.get(
+        api_version="authentication.k8s.io/v1", kind="SelfSubjectReview"
+    )
+    self_subject_review: ResourceInstance = dyn_client.create(self_subject_review_resource)
+    username: str = self_subject_review.status.userInfo.username
+    return username
path: tests/workbenches/utils.py
user: login: rnetser
body: wouldn'r running `oc whoami`  with run_command be easier?
created_at: 2025-02-27T14:53:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route.host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(kind_dict=yaml.safe_load(notebook_string))
+
+
+def get_username(dyn_client: DynamicClient) -> str:
+    """Gets the username for the client (see kubectl -v8 auth whoami)"""
+    self_subject_review_resource: Resource = dyn_client.resources.get(
+        api_version="authentication.k8s.io/v1", kind="SelfSubjectReview"
+    )
+    self_subject_review: ResourceInstance = dyn_client.create(self_subject_review_resource)
+    username: str = self_subject_review.status.userInfo.username
+    return username
+
+
+@contextmanager
+def step(description: str) -> Generator[None, None, None]:
+    yield
path: tests/workbenches/utils.py
user: login: rnetser
body: can you please explain why this is needed?
created_at: 2025-02-27T14:53:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -107,6 +107,7 @@ class KserveAuth:
 
     class OpenDataHubIo:
         MANAGED: str = "opendatahub.io/managed"
+        SERVICE_MESH: str = "opendatahub.io/service-mesh"
path: utilities/constants.py
user: login: rnetser
body: please save `opendatahub.io` and re-use
created_at: 2025-02-27T14:54:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -107,6 +107,7 @@ class KserveAuth:
 
     class OpenDataHubIo:
         MANAGED: str = "opendatahub.io/managed"
+        SERVICE_MESH: str = "opendatahub.io/service-mesh"
path: utilities/constants.py
user: login: jiridanek
body: done
created_at: 2025-02-27T19:20:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route.host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(kind_dict=yaml.safe_load(notebook_string))
+
+
+def get_username(dyn_client: DynamicClient) -> str:
+    """Gets the username for the client (see kubectl -v8 auth whoami)"""
+    self_subject_review_resource: Resource = dyn_client.resources.get(
+        api_version="authentication.k8s.io/v1", kind="SelfSubjectReview"
+    )
+    self_subject_review: ResourceInstance = dyn_client.create(self_subject_review_resource)
+    username: str = self_subject_review.status.userInfo.username
+    return username
+
+
+@contextmanager
+def step(description: str) -> Generator[None, None, None]:
+    yield
path: tests/workbenches/utils.py
user: login: jiridanek
body: so that with step() that I had in the test does not throw exception that step() is not defined

I've removed the definition and all usages now
created_at: 2025-02-27T19:20:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route.host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(kind_dict=yaml.safe_load(notebook_string))
+
+
+def get_username(dyn_client: DynamicClient) -> str:
+    """Gets the username for the client (see kubectl -v8 auth whoami)"""
+    self_subject_review_resource: Resource = dyn_client.resources.get(
+        api_version="authentication.k8s.io/v1", kind="SelfSubjectReview"
+    )
+    self_subject_review: ResourceInstance = dyn_client.create(self_subject_review_resource)
+    username: str = self_subject_review.status.userInfo.username
+    return username
path: tests/workbenches/utils.py
user: login: jiridanek
body: I don't think so, this way I should be able to use the same function with unpriviliged_client in some future test
created_at: 2025-02-27T19:21:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done
created_at: 2025-02-27T19:21:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
+            notebook_image: str = get_notebook_image(image_name="jupyter-minimal-notebook", image_tag="2024.2")
+            notebook = load_default_notebook(
+                dyn_client=admin_client, namespace=self.NTB_NAMESPACE, name=self.NTB_NAME, image=notebook_image
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: if you say so
created_at: 2025-02-27T19:21:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: removed `with step()`
created_at: 2025-02-27T19:21:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
+    """
+
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+                {"name": NTB_NAME},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        with step("Create Notebook CR"):
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: just fancy documentation; deleted that part
created_at: 2025-02-27T19:22:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+from tests.workbenches.utils import get_notebook_image, load_default_notebook, step
+
+
+class TestNotebook:
+    """
+    # description
+    Verifies deployments of Notebooks via GitOps approach
+
+    # before_test_steps
+        1. Step(value="Deploy Pipelines Operator", expected="Pipelines operator is available on the cluster"),
+        2. Step(value="Deploy ServiceMesh Operator", expected="ServiceMesh operator is available on the cluster"),
+        3. Step(value="Deploy Serverless Operator", expected="Serverless operator is available on the cluster"),
+        4. Step(value="Install ODH operator", expected="Operator is up and running and is able to serve it's operands"),
+        5. Step(value="Deploy DSCI", expected="DSCI is created and ready"),
+        6. Step(value="Deploy DSC", expected="DSC is created and ready"),
+
+    # after_test_steps
+        1. Step(value="Delete all created resources", expected="All created resources are removed"),
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: removed
created_at: 2025-02-27T19:22:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: saw no reason to remove it, more like; the code originally comes from odh-e2e repo

guess the world will not end in fire if I delete the header; deleted it now
created_at: 2025-02-27T19:22:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
path: tests/workbenches/utils.py
user: login: jiridanek
body: done
created_at: 2025-02-27T19:44:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
path: tests/workbenches/utils.py
user: login: jiridanek
body: done
created_at: 2025-02-27T19:50:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,61 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route_host: str = list(
+        Route.get(dyn_client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    )[0].host
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route_host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(yaml_file=io.StringIO(notebook_string))
path: tests/workbenches/utils.py
user: login: jiridanek
body: changed to using dict
created_at: 2025-02-27T19:52:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
path: tests/workbenches/conftest.py
user: login: jiridanek
body: such refactoring in my very first PR is a bit too ambitious for me, I'd rather not do it <img src="https://github.com/user-attachments/assets/f0897767-7017-43a3-b729-0c0d0e0a6400" alt="please" width="40"/>

created_at: 2025-03-05T12:31:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jstourac
body: Are you sure we still want this here?
created_at: 2025-03-05T13:57:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: yes, original credits where credit is due
created_at: 2025-03-05T15:26:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import io
+import pathlib
+
+import kubernetes.client
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.route import Route
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step
+from tests.workbenches.resources import Notebook
+from tests.workbenches.utils import step, PodUtils, OcpResourceManager
+from utilities import constants
+
+
+@SuiteDoc(
+    description=Desc("Verifies deployments of Notebooks via GitOps approach"),
+    before_test_steps={
+        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"),
+    },
+    after_test_steps={
+        Step(
+            value="Delete all created resources",
+            expected="All created resources are removed",
+        )
+    },
+)
+class TestNotebookST:
+    NTB_NAME: str = "test-odh-notebook"
+    NTB_NAMESPACE: str = "test-odh-notebook"
+
+    @pytest.mark.parametrize(
+        "users_namespace",
+        [
+            pytest.param(
+                {"name": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @pytest.mark.parametrize(
+        "users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": NTB_NAME, "namespace": NTB_NAMESPACE},
+            )
+        ],
+        indirect=True,
+    )
+    @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",
+            ),
+        },
+    )
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: turned this into a comment
created_at: 2025-03-05T15:28:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+apiVersion: kubeflow.org/v1
+kind: Notebook
path: tests/workbenches/notebook-controller/test_data/notebook.yaml
user: login: jiridanek
body: ended up rewriting it into python dict
created_at: 2025-03-05T15:30:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route.host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(kind_dict=yaml.safe_load(notebook_string))
+
+
+def get_username(dyn_client: DynamicClient) -> str:
+    """Gets the username for the client (see kubectl -v8 auth whoami)"""
+    self_subject_review_resource: Resource = dyn_client.resources.get(
+        api_version="authentication.k8s.io/v1", kind="SelfSubjectReview"
+    )
+    self_subject_review: ResourceInstance = dyn_client.create(self_subject_review_resource)
+    username: str = self_subject_review.status.userInfo.username
+    return username
path: tests/workbenches/utils.py
user: login: rnetser
body: not sure i follow; if you use unprivileged client then this is the user you are logged in with
created_at: 2025-03-17T19:44:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: can you please explain why this is needed?
created_at: 2025-03-17T19:46:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: these are covered by the fixtures; the tests in repo in general do not include these steps but rather have a brief explanation about the test itself
created_at: 2025-03-17T19:47:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        image_name = (
+            "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
+        )
+        notebook_image: str = (
+            f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{image_name}:{'2024.2'}"
+        )
+        notebook = load_default_notebook(
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: this should either be a fixture (as setup for this test) or a separate test; each test should do one thing.
created_at: 2025-03-17T19:48:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
path: tests/workbenches/utils.py
user: login: rnetser
body: please add docstring
created_at: 2025-03-17T19:48:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
path: tests/workbenches/utils.py
user: login: rnetser
body: please raise k8s `ResourceNotFoundError`
created_at: 2025-03-17T19:50:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+
+    notebook = {
+        "apiVersion": "kubeflow.org/v1",
+        "kind": "Notebook",
+        "metadata": {
+            "annotations": {
+                "notebooks.opendatahub.io/inject-oauth": "true",
+                "opendatahub.io/accelerator-name": "",
+                "opendatahub.io/service-mesh": "false",
+            },
+            "labels": {
+                "app": name,
+                "opendatahub.io/dashboard": "true",
path: tests/workbenches/utils.py
user: login: rnetser
body: ```suggestion
                Labels.OpenDataHub.DASHBOARD: "true",
```
created_at: 2025-03-17T19:51:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+
+    notebook = {
+        "apiVersion": "kubeflow.org/v1",
+        "kind": "Notebook",
+        "metadata": {
+            "annotations": {
+                "notebooks.opendatahub.io/inject-oauth": "true",
+                "opendatahub.io/accelerator-name": "",
+                "opendatahub.io/service-mesh": "false",
+            },
+            "labels": {
+                "app": name,
+                "opendatahub.io/dashboard": "true",
+                "opendatahub.io/odh-managed": "true",
+                "sidecar.istio.io/inject": "false",
+            },
+            "name": name,
+            "namespace": namespace,
+        },
+        "spec": {
+            "template": {
+                "spec": {
+                    "affinity": {},
+                    "containers": [
+                        {
+                            "env": [
+                                {
+                                    "name": "NOTEBOOK_ARGS",
+                                    "value": "--ServerApp.port=8888\n"
+                                    "                  "
+                                    "--ServerApp.token=''\n"
+                                    "                  "
+                                    "--ServerApp.password=''\n"
+                                    "                  "
+                                    f"--ServerApp.base_url=/notebook/{namespace}/{name}\n"
+                                    "                  "
+                                    "--ServerApp.quit_button=False\n"
+                                    "                  "
+                                    f'--ServerApp.tornado_settings={{"user":"{username}","hub_host":"https://{route.host}","hub_prefix":"/projects/{namespace}"}}',  # noqa: E501 line too long
+                                },
+                                {"name": "JUPYTER_IMAGE", "value": image},
+                            ],
+                            "image": image,
+                            "imagePullPolicy": "Always",
+                            "livenessProbe": {
+                                "failureThreshold": 3,
+                                "httpGet": {
+                                    "path": f"/notebook/{namespace}/{name}/api",
+                                    "port": "notebook-port",
+                                    "scheme": "HTTP",
+                                },
+                                "initialDelaySeconds": 10,
+                                "periodSeconds": 5,
+                                "successThreshold": 1,
+                                "timeoutSeconds": 1,
path: tests/workbenches/utils.py
user: login: rnetser
body: this can be saved and re-used
created_at: 2025-03-17T19:52:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
path: tests/workbenches/conftest.py
user: login: rnetser
body: please see https://github.com/opendatahub-io/opendatahub-tests/pull/184
created_at: 2025-03-17T20:00:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: deleted steps
created_at: 2025-03-17T20:11:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
path: tests/workbenches/utils.py
user: login: jiridanek
body: done
created_at: 2025-03-17T20:12:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
path: tests/workbenches/utils.py
user: login: jiridanek
body: raised
created_at: 2025-03-17T20:14:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+
+    notebook = {
+        "apiVersion": "kubeflow.org/v1",
+        "kind": "Notebook",
+        "metadata": {
+            "annotations": {
+                "notebooks.opendatahub.io/inject-oauth": "true",
+                "opendatahub.io/accelerator-name": "",
+                "opendatahub.io/service-mesh": "false",
+            },
+            "labels": {
+                "app": name,
+                "opendatahub.io/dashboard": "true",
path: tests/workbenches/utils.py
user: login: jiridanek
body: done
created_at: 2025-03-17T20:16:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: deleted
created_at: 2025-03-17T20:20:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+import pathlib
+from contextlib import contextmanager
+from typing import Generator
+
+import yaml
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+def get_notebook_image(image_name: str, image_tag: str) -> str:
+    controllers_namespace = py_config["applications_namespace"]
+    if py_config.get("distribution") == "upstream":
+        image_dict = {"jupyter-minimal-notebook": "jupyter-minimal-notebook"}
+    else:
+        image_dict = {"jupyter-minimal-notebook": "s2i-minimal-notebook"}
+    return f"{INTERNAL_IMAGE_REGISTRY_PATH}/{controllers_namespace}/{image_dict[image_name]}:{image_tag}"
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    notebook_string = (pathlib.Path(__file__).parent / "notebook-controller/test_data/notebook.yaml").read_text()
+    notebook_string = notebook_string.replace("my-project", namespace).replace("my-workbench", name)
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+    notebook_string = notebook_string.replace("odh_dashboard_route", "https://" + route.host)
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+    notebook_string = notebook_string.replace("odh_user", username)
+    # Replace image
+    notebook_string = notebook_string.replace("notebook_image_placeholder", image)
+
+    return Notebook(kind_dict=yaml.safe_load(notebook_string))
+
+
+def get_username(dyn_client: DynamicClient) -> str:
+    """Gets the username for the client (see kubectl -v8 auth whoami)"""
+    self_subject_review_resource: Resource = dyn_client.resources.get(
+        api_version="authentication.k8s.io/v1", kind="SelfSubjectReview"
+    )
+    self_subject_review: ResourceInstance = dyn_client.create(self_subject_review_resource)
+    username: str = self_subject_review.status.userInfo.username
+    return username
path: tests/workbenches/utils.py
user: login: jiridanek
body: I can be using unprivileged and admin client both at the same time in these tests

In commandline I'm logged as only one of them at a time
created_at: 2025-03-17T20:21:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,175 @@
+from __future__ import annotations
+
+from pytest_testconfig import config as py_config
+
+from kubernetes.dynamic import DynamicClient, Resource, ResourceInstance
+
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+def load_default_notebook(dyn_client: DynamicClient, namespace: str, name: str, image: str) -> Notebook:
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=dyn_client, name=route_name, namespace=py_config["applications_namespace"])
+    assert route.exists, f"Route {route.name} does not exist"
+
+    # Set the correct username
+    username = get_username(dyn_client=dyn_client)
+
+    notebook = {
+        "apiVersion": "kubeflow.org/v1",
+        "kind": "Notebook",
+        "metadata": {
+            "annotations": {
+                "notebooks.opendatahub.io/inject-oauth": "true",
+                "opendatahub.io/accelerator-name": "",
+                "opendatahub.io/service-mesh": "false",
+            },
+            "labels": {
+                "app": name,
+                "opendatahub.io/dashboard": "true",
+                "opendatahub.io/odh-managed": "true",
+                "sidecar.istio.io/inject": "false",
+            },
+            "name": name,
+            "namespace": namespace,
+        },
+        "spec": {
+            "template": {
+                "spec": {
+                    "affinity": {},
+                    "containers": [
+                        {
+                            "env": [
+                                {
+                                    "name": "NOTEBOOK_ARGS",
+                                    "value": "--ServerApp.port=8888\n"
+                                    "                  "
+                                    "--ServerApp.token=''\n"
+                                    "                  "
+                                    "--ServerApp.password=''\n"
+                                    "                  "
+                                    f"--ServerApp.base_url=/notebook/{namespace}/{name}\n"
+                                    "                  "
+                                    "--ServerApp.quit_button=False\n"
+                                    "                  "
+                                    f'--ServerApp.tornado_settings={{"user":"{username}","hub_host":"https://{route.host}","hub_prefix":"/projects/{namespace}"}}',  # noqa: E501 line too long
+                                },
+                                {"name": "JUPYTER_IMAGE", "value": image},
+                            ],
+                            "image": image,
+                            "imagePullPolicy": "Always",
+                            "livenessProbe": {
+                                "failureThreshold": 3,
+                                "httpGet": {
+                                    "path": f"/notebook/{namespace}/{name}/api",
+                                    "port": "notebook-port",
+                                    "scheme": "HTTP",
+                                },
+                                "initialDelaySeconds": 10,
+                                "periodSeconds": 5,
+                                "successThreshold": 1,
+                                "timeoutSeconds": 1,
path: tests/workbenches/utils.py
user: login: jiridanek
body: it think it aint a good idea, but... done
created_at: 2025-03-17T20:32:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import pytest
+from pytest_testconfig import config as py_config
+
+from ocp_resources.pod import Pod
+
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+from tests.workbenches.utils import load_default_notebook
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "users_namespace, users_persistent_volume_claim",
+        [
+            pytest.param(
+                {"name": "test-odh-notebook"},
+                {"name": "test-odh-notebook"},
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self, admin_client, unprivileged_client, users_namespace, users_persistent_volume_claim
+    ):
+        """
+        # description
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+
+        # contact
+        Contact(name="Jakub Stejskal", email="jstejska@redhat.com"),
+
+        # steps
+            1. Step(
+                value="Create namespace for Notebook resources with proper name, labels and annotations",
+                expected="Namespace is created",
+            ),
+            2. Step(value="Create PVC with proper labels and data for Notebook", expected="PVC is created"),
+            3. Step(
+                value="Create Notebook resource with Jupyter Minimal image in pre-defined namespace",
+                expected="Notebook resource is created",
+            ),
+            4. Step(
+                value="Wait for Notebook pods readiness",
+                expected="Notebook pods are up and running, Notebook is in ready state",
+            ),
+        """
+        image_name = (
+            "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
+        )
+        notebook_image: str = (
+            f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{image_name}:{'2024.2'}"
+        )
+        notebook = load_default_notebook(
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: done, separate fixture
created_at: 2025-03-17T20:55:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+#
+# Copyright Skodjob authors.
+# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+#
+from __future__ import annotations
+
+from typing import Generator
+
+from kubernetes.dynamic import DynamicClient
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+import pytest
+
+from utilities import constants
+from utilities.infra import create_ns
+
+
+@pytest.fixture(scope="class")
+def users_namespace(
path: tests/workbenches/conftest.py
user: login: jiridanek
body: seen and adopted, thanks
created_at: 2025-03-18T12:19:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,203 @@
+from __future__ import annotations
+
+from typing import Generator
+
+import pytest
+from pytest_testconfig import config as py_config
+
+
+from tests.workbenches.utils import get_username
+
+from kubernetes.dynamic import DynamicClient
+from kubernetes.dynamic.exceptions import ResourceNotFoundError
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+from utilities.constants import Labels
+from utilities import constants
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
+    request: pytest.FixtureRequest, unprivileged_model_namespace: Namespace, unprivileged_client: DynamicClient
+) -> Generator[PersistentVolumeClaim, None, None]:
+    with PersistentVolumeClaim(
+        client=unprivileged_client,
+        name=request.param["name"],
+        namespace=unprivileged_model_namespace.name,
+        label={constants.Labels.OpenDataHub.DASHBOARD: "true"},
+        accessmodes=PersistentVolumeClaim.AccessMode.RWO,
+        size="10Gi",
+        volume_mode=PersistentVolumeClaim.VolumeMode.FILE,
+    ) as pvc:
+        yield pvc
+
+
+@pytest.fixture(scope="function")
+def minimal_image() -> Generator[str, None, None]:
+    """Provides a full image name of a minimal workbench image"""
+    image_name = "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
+    yield f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{image_name}:{'2024.2'}"
+
+
+@pytest.fixture(scope="function")
+def default_notebook(
+    request: pytest.FixtureRequest,
+    admin_client: DynamicClient,
+    minimal_image: str,
+) -> Generator[Notebook, None, None]:
+    """Returns a new Notebook CR for a given namespace, name, and image"""
+    namespace = request.param["namespace"]
+    name = request.param["name"]
+
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=admin_client, name=route_name, namespace=py_config["applications_namespace"])
+    if not route.exists:
+        raise ResourceNotFoundError(f"Route {route.name} does not exist")
+
+    # Set the correct username
+    username = get_username(dyn_client=admin_client)
+
+    probe_config = {
path: tests/workbenches/conftest.py
user: login: lugi0
body: I'm wondering how much of what is in these two dicts you expect to be dynamic vs constant. If at all possible I'd remove the portions that can be considered constant across different notebooks to make this slightly less verbose. Otherwise LGTM.
created_at: 2025-03-18T17:34:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,203 @@
+from __future__ import annotations
+
+from typing import Generator
+
+import pytest
+from pytest_testconfig import config as py_config
+
+
+from tests.workbenches.utils import get_username
+
+from kubernetes.dynamic import DynamicClient
+from kubernetes.dynamic.exceptions import ResourceNotFoundError
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+from utilities.constants import Labels
+from utilities import constants
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
+    request: pytest.FixtureRequest, unprivileged_model_namespace: Namespace, unprivileged_client: DynamicClient
+) -> Generator[PersistentVolumeClaim, None, None]:
+    with PersistentVolumeClaim(
+        client=unprivileged_client,
+        name=request.param["name"],
+        namespace=unprivileged_model_namespace.name,
+        label={constants.Labels.OpenDataHub.DASHBOARD: "true"},
+        accessmodes=PersistentVolumeClaim.AccessMode.RWO,
+        size="10Gi",
+        volume_mode=PersistentVolumeClaim.VolumeMode.FILE,
+    ) as pvc:
+        yield pvc
+
+
+@pytest.fixture(scope="function")
+def minimal_image() -> Generator[str, None, None]:
+    """Provides a full image name of a minimal workbench image"""
+    image_name = "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
+    yield f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{image_name}:{'2024.2'}"
+
+
+@pytest.fixture(scope="function")
+def default_notebook(
+    request: pytest.FixtureRequest,
+    admin_client: DynamicClient,
+    minimal_image: str,
+) -> Generator[Notebook, None, None]:
+    """Returns a new Notebook CR for a given namespace, name, and image"""
+    namespace = request.param["namespace"]
+    name = request.param["name"]
+
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=admin_client, name=route_name, namespace=py_config["applications_namespace"])
+    if not route.exists:
+        raise ResourceNotFoundError(f"Route {route.name} does not exist")
+
+    # Set the correct username
+    username = get_username(dyn_client=admin_client)
+
+    probe_config = {
path: tests/workbenches/conftest.py
user: login: jiridanek
body: I'm not worried the maintenance will be too much of a burden on me. I'm thinking this may end up not being touched again at all, for years!
created_at: 2025-03-19T17:53:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "unprivileged_model_namespace,users_persistent_volume_claim,default_notebook",
+        [
+            pytest.param(
+                {
+                    "name": "test-odh-notebook",
+                    "dashboard-label": True,
+                },
+                {"name": "test-odh-notebook"},
+                {
+                    "namespace": "test-odh-notebook",
+                    "name": "test-odh-notebook",
+                },
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self,
+        admin_client,
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: not used, please remove
created_at: 2025-03-20T14:45:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "unprivileged_model_namespace,users_persistent_volume_claim,default_notebook",
+        [
+            pytest.param(
+                {
+                    "name": "test-odh-notebook",
+                    "dashboard-label": True,
+                },
+                {"name": "test-odh-notebook"},
+                {
+                    "namespace": "test-odh-notebook",
+                    "name": "test-odh-notebook",
+                },
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self,
+        admin_client,
+        unprivileged_client,
+        unprivileged_model_namespace,
+        users_persistent_volume_claim,
+        default_notebook,
+    ):
+        """
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+        """
+        with default_notebook:
+            pods = Pod.get(
+                dyn_client=unprivileged_client,
+                namespace=unprivileged_model_namespace.name,
+                label_selector=f"app={unprivileged_model_namespace.name}",
+            )
+            assert pods, "The expected notebook pods were not found"
+            for pod in pods:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: for debug i think it will be better to have a function which aggregates all failed pods and then raise and list all pod names. wdyt?
created_at: 2025-03-20T14:48:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "unprivileged_model_namespace,users_persistent_volume_claim,default_notebook",
+        [
+            pytest.param(
+                {
+                    "name": "test-odh-notebook",
+                    "dashboard-label": True,
+                },
+                {"name": "test-odh-notebook"},
+                {
+                    "namespace": "test-odh-notebook",
+                    "name": "test-odh-notebook",
+                },
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self,
+        admin_client,
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: removed
created_at: 2025-03-20T14:55:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import pytest
+
+from ocp_resources.pod import Pod
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "unprivileged_model_namespace,users_persistent_volume_claim,default_notebook",
+        [
+            pytest.param(
+                {
+                    "name": "test-odh-notebook",
+                    "dashboard-label": True,
+                },
+                {"name": "test-odh-notebook"},
+                {
+                    "namespace": "test-odh-notebook",
+                    "name": "test-odh-notebook",
+                },
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self,
+        admin_client,
+        unprivileged_client,
+        unprivileged_model_namespace,
+        users_persistent_volume_claim,
+        default_notebook,
+    ):
+        """
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+        """
+        with default_notebook:
+            pods = Pod.get(
+                dyn_client=unprivileged_client,
+                namespace=unprivileged_model_namespace.name,
+                label_selector=f"app={unprivileged_model_namespace.name}",
+            )
+            assert pods, "The expected notebook pods were not found"
+            for pod in pods:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: collected all failed_pods
created_at: 2025-03-20T14:56:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import pytest
+
+from timeout_sampler import TimeoutExpiredError
+
+from ocp_resources.pod import Pod
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "unprivileged_model_namespace,users_persistent_volume_claim,default_notebook",
+        [
+            pytest.param(
+                {
+                    "name": "test-odh-notebook",
+                    "dashboard-label": True,
+                },
+                {"name": "test-odh-notebook"},
+                {
+                    "namespace": "test-odh-notebook",
+                    "name": "test-odh-notebook",
+                },
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self,
+        unprivileged_client,
+        unprivileged_model_namespace,
+        users_persistent_volume_claim,
+        default_notebook,
+    ):
+        """
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+        """
+        with default_notebook:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: rnetser
body: sorry, missed this. not needed as `default_notebook` yields the object which will be cleaned up on teardown
created_at: 2025-03-20T15:34:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import pytest
+
+from timeout_sampler import TimeoutExpiredError
+
+from ocp_resources.pod import Pod
+
+
+class TestNotebook:
+    @pytest.mark.parametrize(
+        "unprivileged_model_namespace,users_persistent_volume_claim,default_notebook",
+        [
+            pytest.param(
+                {
+                    "name": "test-odh-notebook",
+                    "dashboard-label": True,
+                },
+                {"name": "test-odh-notebook"},
+                {
+                    "namespace": "test-odh-notebook",
+                    "name": "test-odh-notebook",
+                },
+            )
+        ],
+        indirect=True,
+    )
+    def test_create_simple_notebook(
+        self,
+        unprivileged_client,
+        unprivileged_model_namespace,
+        users_persistent_volume_claim,
+        default_notebook,
+    ):
+        """
+        Create simple Notebook with all needed resources and see if Operator creates it properly
+        """
+        with default_notebook:
path: tests/workbenches/notebook-controller/test_spawning.py
user: login: jiridanek
body: fixed, thanks
created_at: 2025-03-20T15:48:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,204 @@
+from __future__ import annotations
+
+from typing import Generator
+
+import pytest
+from pytest_testconfig import config as py_config
+
+
+from tests.workbenches.utils import get_username
+
+from kubernetes.dynamic import DynamicClient
+from kubernetes.dynamic.exceptions import ResourceNotFoundError
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+from utilities.constants import Labels
+from utilities import constants
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
+    request: pytest.FixtureRequest, unprivileged_model_namespace: Namespace, unprivileged_client: DynamicClient
+) -> Generator[PersistentVolumeClaim, None, None]:
+    with PersistentVolumeClaim(
+        client=unprivileged_client,
+        name=request.param["name"],
+        namespace=unprivileged_model_namespace.name,
+        label={constants.Labels.OpenDataHub.DASHBOARD: "true"},
+        accessmodes=PersistentVolumeClaim.AccessMode.RWO,
+        size="10Gi",
+        volume_mode=PersistentVolumeClaim.VolumeMode.FILE,
+    ) as pvc:
+        yield pvc
+
+
+@pytest.fixture(scope="function")
+def minimal_image() -> Generator[str, None, None]:
+    """Provides a full image name of a minimal workbench image"""
+    image_name = "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
+    yield f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{image_name}:{'2024.2'}"
+
+
+@pytest.fixture(scope="function")
+def default_notebook(
+    request: pytest.FixtureRequest,
+    admin_client: DynamicClient,
+    minimal_image: str,
+) -> Generator[Notebook, None, None]:
+    """Returns a new Notebook CR for a given namespace, name, and image"""
+    namespace = request.param["namespace"]
+    name = request.param["name"]
+
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=admin_client, name=route_name, namespace=py_config["applications_namespace"])
+    if not route.exists:
+        raise ResourceNotFoundError(f"Route {route.name} does not exist")
+
+    # Set the correct username
+    username = get_username(dyn_client=admin_client)
+
+    probe_config = {
path: tests/workbenches/conftest.py
user: login: mwaykole
body: cant we get it from cluster or any other place ?
created_at: 2025-03-20T15:56:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,204 @@
+from __future__ import annotations
+
+from typing import Generator
+
+import pytest
+from pytest_testconfig import config as py_config
+
+
+from tests.workbenches.utils import get_username
+
+from kubernetes.dynamic import DynamicClient
+from kubernetes.dynamic.exceptions import ResourceNotFoundError
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+from ocp_resources.route import Route
+from ocp_resources.notebook import Notebook
+
+
+from utilities.constants import Labels
+from utilities import constants
+from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
+
+
+@pytest.fixture(scope="function")
+def users_persistent_volume_claim(
+    request: pytest.FixtureRequest, unprivileged_model_namespace: Namespace, unprivileged_client: DynamicClient
+) -> Generator[PersistentVolumeClaim, None, None]:
+    with PersistentVolumeClaim(
+        client=unprivileged_client,
+        name=request.param["name"],
+        namespace=unprivileged_model_namespace.name,
+        label={constants.Labels.OpenDataHub.DASHBOARD: "true"},
+        accessmodes=PersistentVolumeClaim.AccessMode.RWO,
+        size="10Gi",
+        volume_mode=PersistentVolumeClaim.VolumeMode.FILE,
+    ) as pvc:
+        yield pvc
+
+
+@pytest.fixture(scope="function")
+def minimal_image() -> Generator[str, None, None]:
+    """Provides a full image name of a minimal workbench image"""
+    image_name = "jupyter-minimal-notebook" if py_config.get("distribution") == "upstream" else "s2i-minimal-notebook"
+    yield f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{image_name}:{'2024.2'}"
+
+
+@pytest.fixture(scope="function")
+def default_notebook(
+    request: pytest.FixtureRequest,
+    admin_client: DynamicClient,
+    minimal_image: str,
+) -> Generator[Notebook, None, None]:
+    """Returns a new Notebook CR for a given namespace, name, and image"""
+    namespace = request.param["namespace"]
+    name = request.param["name"]
+
+    # Set new Route url
+    route_name = "odh-dashboard" if py_config.get("distribution") == "upstream" else "rhods-dashboard"
+    route = Route(client=admin_client, name=route_name, namespace=py_config["applications_namespace"])
+    if not route.exists:
+        raise ResourceNotFoundError(f"Route {route.name} does not exist")
+
+    # Set the correct username
+    username = get_username(dyn_client=admin_client)
+
+    probe_config = {
path: tests/workbenches/conftest.py
user: login: jiridanek
body: no

1. the precise values don't matter all that much
2. source of truth lives in odh-dashboard in the backend typescript code

https://github.com/opendatahub-io/odh-dashboard/blob/700a4fa82e06d37e08d37fd98e29049577b99f6b/backend/src/utils/notebookUtils.ts#L273-L298
created_at: 2025-03-20T16:01:06Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-01-15T17:04:21Z
body: 
The following are automatically added/executed:
 * PR size label.
 * Run [pre-commit](https://pre-commit.ci/)
 * Run [tox](https://tox.wiki/)

Available user actions:
 * To mark a PR as `WIP`, add `/wip` in a comment. To remove it from the PR comment `/wip cancel` to the PR.
 * To block merging of a PR, add `/hold` in a comment. To un-block merging of PR comment `/hold cancel`.
 * To mark a PR as approved, add `/lgtm` in a comment. To remove, add `/lgtm cancel`.
        `lgtm` label removed on each new commit push.
 * To mark PR as verified comment `/verified` to the PR, to un-verify comment `/verified cancel` to the PR.
        `verified` label removed on each new commit push.

<details>
<summary>Supported labels</summary>

{'/hold', '/verified', '/lgtm', '/wip'}
</details>
    
author_association: NONE
user: login: jstourac
created_at: 2025-03-05T14:11:54Z
body: Thank you for this, Jiri. This LGTM, I put one nitpick comment, but I have no objections truly.

What is this currently stuck on? Also, it's quite hard to navigate here through so many comments. Would it be possible to resolve those that are addressed already? Anyone with the privileges to do so? :angel: 
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-17T08:01:46Z
body: /label tide/merge-method-squash
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-17T13:54:31Z
body: @jstourac squashed the fixup commits together
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-18T12:16:34Z
body: /verified
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-19T17:59:26Z
body: /verified

@rnetser @lugi0 rebased, should be ready!
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-20T14:55:17Z
body: /verified
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-20T15:48:44Z
body: /verified
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-21T11:07:51Z
body: /verified
author_association: MEMBER
user: login: jiridanek
created_at: 2025-03-23T10:23:49Z
body: /verified
author_association: MEMBER