From 87c56c8a769687e470a9557b6e10f7106a1f8f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Wed, 15 Jan 2025 18:03:37 +0100 Subject: [PATCH 1/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci RHOAIENG-16055: new(tests): test to start a Workbench, by creating the Notebook CR directly --- pyproject.toml | 1 + tests/workbenches/__init__.py | 0 tests/workbenches/conftest.py | 133 ++++++++++++ tests/workbenches/docs.py | 32 +++ .../test_data/notebook.yaml | 146 +++++++++++++ .../notebook-controller/test_spawning.py | 204 ++++++++++++++++++ 6 files changed, 516 insertions(+) create mode 100644 tests/workbenches/__init__.py create mode 100644 tests/workbenches/conftest.py create mode 100644 tests/workbenches/docs.py create mode 100644 tests/workbenches/notebook-controller/test_data/notebook.yaml create mode 100644 tests/workbenches/notebook-controller/test_spawning.py diff --git a/pyproject.toml b/pyproject.toml index df81d6b..5ab91fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dependencies = [ "pytest-testconfig>=0.2.0", "pytest-jira>=0.3.21", "pygithub>=2.5.0", + "allure-pytest>=2.13.5", ] [project.urls] diff --git a/tests/workbenches/__init__.py b/tests/workbenches/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/workbenches/conftest.py b/tests/workbenches/conftest.py new file mode 100644 index 0000000..338796d --- /dev/null +++ b/tests/workbenches/conftest.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +from kubernetes.dynamic import DynamicClient + +import ocp_resources.resource + +import pytest + + +@pytest.fixture(scope="function") +def function_resource_manager(admin_client: DynamicClient) -> KubeResourceManager: + resource_manager = KubeResourceManager(admin_client) + yield resource_manager + resource_manager.destroy() + + +class KubeResourceManager: + def __init__(self, privileged_client: DynamicClient): + self.privileged_client = privileged_client + self.resources: list[ocp_resources.resource.Resource] = [] + + def createResourceWithoutWait(self, client: DynamicClient, resource: ocp_resources.resource.Resource): + resource.client = self.privileged_client + resource.create() + self.resources.append(resource) + + def createResourceWithWait(self, client: DynamicClient, resource: ocp_resources.resource.Resource): + resource.client = self.privileged_client + resource.create() + self.resources.append(resource) + + def destroy(self): + for resource in self.resources: + # self.privileged_client.delete(resource) + resource.delete() + + +class OdhAnnotationsLabels: + OPENSHIFT_DOMAIN = "openshift.io/" + ODH_DOMAIN = "opendatahub.io/" + + LABEL_DASHBOARD = ODH_DOMAIN + "dashboard" + LABEL_ODH_MANAGED = ODH_DOMAIN + "odh-managed" + LABEL_SIDECAR_ISTIO_INJECT = "sidecar.istio.io/inject" + + ANNO_SERVICE_MESH = ODH_DOMAIN + "service-mesh" + ANNO_MODEL_MESH = "modelmesh-enabled" + ANNO_NTB_INJECT_OAUTH = "notebooks." + ODH_DOMAIN + "inject-oauth" + APP_LABEL_KEY = "app" + APP_LABEL_VALUE = "odh-e2e" + + +def getOdhOrRhoai(name, odh_value, rhoai_value): + return rhoai_value + + +class OdhConstants: + # private static final Logger LOGGER = LoggerFactory.getLogger(OdhConstants.class); + # private static final Map VALUES = new HashMap<>(); + # // ODH + ODH_CONTROLLERS_NAMESPACE: str = "opendatahub" + ODH_DASHBOARD_ROUTE_NAME: str = "odh-dashboard" + # private static final String ODH_DASHBOARD_CONTROLLER = "odh-dashboard"; + # + # private static final String ODH_BUNDLE_OPERATOR_NAME = "opendatahub-operator-system"; + # private static final String ODH_MONITORING_NAMESPACE = "odh-monitoring"; + # + # + # // ODH OLM + # private static final String ODH_OLM_OPERATOR_NAME = "opendatahub-operator"; + # private static final String ODH_OLM_OPERATOR_NAMESPACE = "openshift-operators"; + # private static final String ODH_OLM_OPERATOR_DEPLOYMENT_NAME = "opendatahub-operator-controller-manager"; + # private static final String ODH_OLM_SOURCE_NAME = "community-operators"; + # private static final String ODH_OLM_APP_BUNDLE_PREFIX = "opendatahub-operator"; + # private static final String ODH_OLM_OPERATOR_CHANNEL = "fast"; + # private static final String ODH_DSCI_NAME = "default"; + # // TODO - should be changed after 2.5 release + # private static final String ODH_OLM_OPERATOR_VERSION = "v2.8.0"; + # private static final String ODH_OLM_UPGRADE_STARTING_OPERATOR_VERSION = "v2.8.0"; + # + # // RHOAI + RHOAI_CONTROLLERS_NAMESPACE: str = "redhat-ods-applications" + RHOAI_DASHBOARD_ROUTE_NAME: str = "rhods-dashboard" + # private static final String RHOAI_DASHBOARD_CONTROLLER = "rhods-dashboard"; + # private static final String RHOAI_NOTEBOOKS_NAMESPACE = "rhods-notebooks"; + # private static final String RHOAI_DSCI_NAME = "default-dsci"; + # // RHOAI OLM + # private static final String RHOAI_OLM_OPERATOR_NAME = "rhods-operator"; + # private static final String RHOAI_OLM_OPERATOR_NAMESPACE = "redhat-ods-operator"; + # private static final String RHOAI_OLM_OPERATOR_DEPLOYMENT_NAME = "rhods-operator"; + # private static final String RHOAI_OLM_SOURCE_NAME = "redhat-operators"; + # private static final String RHOAI_OLM_APP_BUNDLE_PREFIX = "rhods-operator"; + # private static final String RHOAI_OLM_OPERATOR_CHANNEL = "fast"; + # private static final String RHOAI_OLM_OPERATOR_VERSION = "2.7.0"; + # private static final String RHOAI_OLM_UPGRADE_STARTING_OPERATOR_VERSION = "2.6.0"; + # private static final String RHOAI_MONITORING_NAMESPACE = "redhat-ods-monitoring"; + # + # // Public part + # public static final String DSC_CREATION_SUCCESSFUL_EVENT_NAME = "DataScienceClusterCreationSuccessful"; + # + # public static final String CODEFLARE_DEPLOYMENT_NAME = "codeflare-operator-manager"; + # public static final String DS_PIPELINES_OPERATOR = "data-science-pipelines-operator-controller-manager"; + # public static final String ETCD = "etcd"; + # public static final String KSERVE_OPERATOR = "kserve-controller-manager"; + # public static final String KUBERAY_OPERATOR = "kuberay-operator"; + # public static final String MODELMESH_OPERATOR = "modelmesh-controller"; + # public static final String NOTEBOOK_OPERATOR = "notebook-controller-deployment"; + # public static final String ODH_MODEL_OPERATOR = "odh-model-controller"; + # public static final String ODH_NOTEBOOK_OPERATOR = "odh-notebook-controller-manager"; + # public static final String KUEUE_OPERATOR = "kueue-controller-manager"; + # public static final String KNATIVE_SERVING_NAMESPACE = "knative-serving"; + # public static final String ISTIO_SYSTEM_NAMESPACE = "istio-system"; + # + CONTROLLERS_NAMESPACE: str = getOdhOrRhoai( + "CONTROLLERS_NAMESPACE", ODH_CONTROLLERS_NAMESPACE, RHOAI_CONTROLLERS_NAMESPACE + ) + DASHBOARD_ROUTE_NAME: str = getOdhOrRhoai( + "DASHBOARD_ROUTE_NAME", ODH_DASHBOARD_ROUTE_NAME, RHOAI_DASHBOARD_ROUTE_NAME + ) + # public static final String DASHBOARD_CONTROLLER = getOdhOrRhoai("DASHBOARD_CONTROLLER", ODH_DASHBOARD_CONTROLLER, RHOAI_DASHBOARD_CONTROLLER); + # public static final String NOTEBOOKS_NAMESPACE = getOdhOrRhoai("NOTEBOOKS_NAMESPACE", ODH_CONTROLLERS_NAMESPACE, RHOAI_NOTEBOOKS_NAMESPACE); + # public static final String BUNDLE_OPERATOR_NAMESPACE = getOdhOrRhoai("BUNDLE_OPERATOR_NAMESPACE", ODH_BUNDLE_OPERATOR_NAME, RHOAI_OLM_OPERATOR_NAME); + # public static final String DEFAULT_DSCI_NAME = getOdhOrRhoai("DSCI_NAME", ODH_DSCI_NAME, RHOAI_DSCI_NAME); + # public static final String MONITORING_NAMESPACE = getOdhOrRhoai("MONITORING_NAMESPACE", ODH_MONITORING_NAMESPACE, RHOAI_MONITORING_NAMESPACE); + # // OLM env variables + # public static final String OLM_OPERATOR_NAME = getOdhOrRhoai("OLM_OPERATOR_NAME", ODH_OLM_OPERATOR_NAME, RHOAI_OLM_OPERATOR_NAME); + # public static final String OLM_OPERATOR_NAMESPACE = getOdhOrRhoai("OLM_OPERATOR_NAMESPACE", ODH_OLM_OPERATOR_NAMESPACE, RHOAI_OLM_OPERATOR_NAMESPACE); + # public static final String OLM_OPERATOR_DEPLOYMENT_NAME = getOdhOrRhoai("OLM_OPERATOR_DEPLOYMENT_NAME", ODH_OLM_OPERATOR_DEPLOYMENT_NAME, RHOAI_OLM_OPERATOR_DEPLOYMENT_NAME); + # public static final String OLM_APP_BUNDLE_PREFIX = getOdhOrRhoai("OLM_APP_BUNDLE_PREFIX", ODH_OLM_APP_BUNDLE_PREFIX, RHOAI_OLM_APP_BUNDLE_PREFIX); + # public static final String OLM_OPERATOR_VERSION = getOdhOrRhoai("OLM_OPERATOR_VERSION", ODH_OLM_OPERATOR_VERSION, RHOAI_OLM_OPERATOR_VERSION); + # public static final String OLM_SOURCE_NAME = getOdhOrRhoai("OLM_SOURCE_NAME", ODH_OLM_SOURCE_NAME, RHOAI_OLM_SOURCE_NAME); + # public static final String OLM_OPERATOR_CHANNEL = getOdhOrRhoai("OLM_OPERATOR_CHANNEL", ODH_OLM_OPERATOR_CHANNEL, RHOAI_OLM_OPERATOR_CHANNEL); + # public static final String OLM_UPGRADE_STARTING_OPERATOR_VERSION = getOdhOrRhoai("OLM_UPGRADE_STARTING_OPERATOR_VERSION", ODH_OLM_UPGRADE_STARTING_OPERATOR_VERSION, RHOAI_OLM_UPGRADE_STARTING_OPERATOR_VERSION); diff --git a/tests/workbenches/docs.py b/tests/workbenches/docs.py new file mode 100644 index 0000000..d84c964 --- /dev/null +++ b/tests/workbenches/docs.py @@ -0,0 +1,32 @@ +def Desc(value: str): + return value + + +def Step( + value: str, + expected: str, +): + return value, expected + + +def SuiteDoc( + description: str, + beforeTestSteps: set[Step], + afterTestSteps: set[Step], +): + return lambda x: x + + +def Contact( + name: str, + email: str, +): + return name, email + + +def TestDoc( + description: str, + contact: str, + steps: set[Step], +): + return lambda x: x diff --git a/tests/workbenches/notebook-controller/test_data/notebook.yaml b/tests/workbenches/notebook-controller/test_data/notebook.yaml new file mode 100644 index 0000000..c43c988 --- /dev/null +++ b/tests/workbenches/notebook-controller/test_data/notebook.yaml @@ -0,0 +1,146 @@ +apiVersion: kubeflow.org/v1 +kind: Notebook +metadata: + annotations: + notebooks.opendatahub.io/inject-oauth: 'true' + opendatahub.io/service-mesh: 'false' + opendatahub.io/accelerator-name: '' + labels: + app: my-workbench + opendatahub.io/dashboard: 'true' + opendatahub.io/odh-managed: 'true' + sidecar.istio.io/inject: 'false' + name: my-workbench + namespace: my-project +spec: + template: + spec: + affinity: {} + containers: + - env: + - name: NOTEBOOK_ARGS + value: |- + --ServerApp.port=8888 + --ServerApp.token='' + --ServerApp.password='' + --ServerApp.base_url=/notebook/my-project/my-workbench + --ServerApp.quit_button=False + --ServerApp.tornado_settings={"user":"odh_user","hub_host":"odh_dashboard_route","hub_prefix":"/projects/my-project"} + - name: JUPYTER_IMAGE + value: notebook_image_placeholder + image: notebook_image_placeholder + imagePullPolicy: Always + livenessProbe: + failureThreshold: 3 + httpGet: + path: /notebook/my-project/my-workbench/api + port: notebook-port + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: my-workbench + ports: + - containerPort: 8888 + name: notebook-port + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /notebook/my-project/my-workbench/api + port: notebook-port + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "1" + memory: 1Gi + volumeMounts: + - mountPath: /opt/app-root/src + name: my-workbench + - mountPath: /dev/shm + name: shm + workingDir: /opt/app-root/src + - args: + - --provider=openshift + - --https-address=:8443 + - --http-address= + - --openshift-service-account=my-workbench + - --cookie-secret-file=/etc/oauth/config/cookie_secret + - --cookie-expire=24h0m0s + - --tls-cert=/etc/tls/private/tls.crt + - --tls-key=/etc/tls/private/tls.key + - --upstream=http://localhost:8888 + - --upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + - --email-domain=* + - --skip-provider-button + - --openshift-sar={"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org","resourceName":"my-workbench","namespace":"$(NAMESPACE)"} + - --logout-url=odh_dashboard_route/projects/my-project?notebookLogout=my-workbench + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: registry.redhat.io/openshift4/ose-oauth-proxy:v4.10 + imagePullPolicy: Always + livenessProbe: + failureThreshold: 3 + httpGet: + path: /oauth/healthz + port: oauth-proxy + scheme: HTTPS + initialDelaySeconds: 30 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: oauth-proxy + ports: + - containerPort: 8443 + name: oauth-proxy + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /oauth/healthz + port: oauth-proxy + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 100m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi + volumeMounts: + - mountPath: /etc/oauth/config + name: oauth-config + - mountPath: /etc/tls/private + name: tls-certificates + enableServiceLinks: false + serviceAccountName: my-workbench + volumes: + - name: my-workbench + persistentVolumeClaim: + claimName: my-workbench + - emptyDir: + medium: Memory + name: shm + - name: oauth-config + secret: + defaultMode: 420 + secretName: my-workbench-oauth-config + - name: tls-certificates + secret: + defaultMode: 420 + secretName: my-workbench-tls diff --git a/tests/workbenches/notebook-controller/test_spawning.py b/tests/workbenches/notebook-controller/test_spawning.py new file mode 100644 index 0000000..2bc98ff --- /dev/null +++ b/tests/workbenches/notebook-controller/test_spawning.py @@ -0,0 +1,204 @@ +# +# 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 +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, 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"): + # LabelSelector lblSelector = new LabelSelectorBuilder() + # .withMatchLabels(Map.of("app", NTB_NAME)) + # .build(); + # + # PodUtils.waitForPodsReady(NTB_NAMESPACE, lblSelector, 1, true, () -> { }); + + +# +# @BeforeAll +# void deployDataScienceCluster() { +# if (Environment.SKIP_DEPLOY_DSCI_DSC) { +# LOGGER.info("DSCI and DSC deploy is skipped"); +# return; +# } +# // Create DSCI +# DSCInitialization dsci = DscUtils.getBasicDSCI(); +# +# // Create DSC +# DataScienceCluster dsc = new DataScienceClusterBuilder() +# .withNewMetadata() +# .withName(DS_PROJECT_NAME) +# .endMetadata() +# .withNewSpec() +# .withComponents( +# new ComponentsBuilder() +# .withWorkbenches( +# new WorkbenchesBuilder().withManagementState(Workbenches.ManagementState.Managed).build() +# ) +# .withDashboard( +# new DashboardBuilder().withManagementState(Dashboard.ManagementState.Managed).build() +# ) +# .withKserve( +# new KserveBuilder().withManagementState(Kserve.ManagementState.Managed).build() +# ) +# .withKueue( +# new KueueBuilder().withManagementState(Kueue.ManagementState.Managed).build() +# ) +# .withCodeflare( +# new CodeflareBuilder().withManagementState(Codeflare.ManagementState.Managed).build() +# ) +# .withDatasciencepipelines( +# new DatasciencepipelinesBuilder().withManagementState(Datasciencepipelines.ManagementState.Managed).build() +# ) +# .withModelmeshserving( +# new ModelmeshservingBuilder().withManagementState(Modelmeshserving.ManagementState.Managed).build() +# ) +# .withRay( +# new RayBuilder().withManagementState(Ray.ManagementState.Managed).build() +# ) +# .build()) +# .endSpec() +# .build(); +# // Deploy DSCI,DSC +# KubeResourceManager.getInstance().createOrUpdateResourceWithWait(dsci); +# KubeResourceManager.getInstance().createResourceWithWait(dsc); +# } +# } + + +def loadDefaultNotebook(client: DynamicClient, namespace: str, name: str, image: str) -> Notebook: + notebookString = pathlib.Path( + "/Users/jdanek/IdeaProjects/opendatahub-tests/tests/workbenches/notebook-controller/test_data/notebook.yaml" + ).read_text() + notebookString = notebookString.replace("my-project", namespace).replace("my-workbench", name) + # Set new Route url + # routeHost: str = cast(ocp_resources.route.Route, client.resources.get(resource=, name=OdhConstants.DASHBOARD_ROUTE_NAME, namespace = OdhConstants.CONTROLLERS_NAMESPACE)).host + routeHost: str = list( + ocp_resources.route.Route.get( + client=client, name=OdhConstants.DASHBOARD_ROUTE_NAME, namespace=OdhConstants.CONTROLLERS_NAMESPACE + ) + )[0].host + notebookString = notebookString.replace("odh_dashboard_route", "https://" + routeHost) + # Set correct username (see kubectl -v8 auth whoami) + self_subject_review_resource: kubernetes.dynamic.Resource = client.resources.get( + api_version="authentication.k8s.io/v1", kind="SelfSubjectReview" + ) + self_subject_review: kubernetes.dynamic.ResourceInstance = client.create(self_subject_review_resource) + username: str = self_subject_review.status.userInfo.username + notebookString = notebookString.replace("odh_user", username) + # Replace image + notebookString = notebookString.replace("notebook_image_placeholder", image) + + nb = Notebook(yaml_file=io.StringIO(notebookString)) + # notebook_resource: kubernetes.dynamic.Resource = client.resources.get(api_version="kubeflow.org/v1", kind="Notebook") + # notebook: kubernetes.dynamic.ResourceInstance = kubernetes.dynamic.ResourceInstance(client=client, instance=yaml.safe_load(notebookString)) + + return nb + # return ocp_resources.resource.NamespacedResource(client=client, api_group="kubeflow.org", yaml_file=io.StringIO(notebookString)) + + +class Notebook(ocp_resources.resource.NamespacedResource): + api_group: str = "kubeflow.org" From c696defa70cba18d6006cae3a9b78da6be07ff0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Wed, 15 Jan 2025 18:03:37 +0100 Subject: [PATCH 2/9] RHOAIENG-16055: new(tests): test to start a Workbench, by creating the Notebook CR directly --- tests/workbenches/conftest.py | 10 +++---- .../notebook-controller/test_spawning.py | 1 + uv.lock | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/tests/workbenches/conftest.py b/tests/workbenches/conftest.py index 338796d..1bc7d5b 100644 --- a/tests/workbenches/conftest.py +++ b/tests/workbenches/conftest.py @@ -111,12 +111,10 @@ class OdhConstants: # 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 - ) + 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); diff --git a/tests/workbenches/notebook-controller/test_spawning.py b/tests/workbenches/notebook-controller/test_spawning.py index 2bc98ff..361937b 100644 --- a/tests/workbenches/notebook-controller/test_spawning.py +++ b/tests/workbenches/notebook-controller/test_spawning.py @@ -16,6 +16,7 @@ import ocp_resources.resource import kubernetes.client +import yaml from kubernetes.dynamic import DynamicClient from tests.workbenches.conftest import OdhAnnotationsLabels, OdhConstants diff --git a/uv.lock b/uv.lock index 596f323..01fa7ca 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,32 @@ resolution-markers = [ "python_full_version >= '3.11'", ] +[[package]] +name = "allure-pytest" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "allure-python-commons" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/c3/829a300b72557e9327fa72692b62917dab3ad5bab678962ea84c066d4eef/allure-pytest-2.13.5.tar.gz", hash = "sha256:0ef8e1790c44a988db6b83c4d4f5e91451e2c4c8ea10601dfa88528d23afcf6e", size = 16976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/7b/430830ed7bf1ef078f9e55eb4b19cc059ef5310a20b5bd73eeb99e2c5ff1/allure_pytest-2.13.5-py3-none-any.whl", hash = "sha256:94130bac32964b78058e62cf4b815ad97a5ac82a065e6dd2d43abac2be7640fc", size = 11606 }, +] + +[[package]] +name = "allure-python-commons" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/96/9991a10dcd25b98c8c3e4c7916780f33f13a55a0efbe8c7ea96505271d91/allure-python-commons-2.13.5.tar.gz", hash = "sha256:a232e7955811f988e49a4c1dd6c16cce7e9b81d0ea0422b1e5654d3254e2caf3", size = 12934 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/18/79a66d6adc301281803398f7288583dff8d1d3f2672c07ffab03ee12c7cd/allure_python_commons-2.13.5-py3-none-any.whl", hash = "sha256:8b0e837b6e32d810adec563f49e1d04127a5b6770e0232065b7cb09b9953980d", size = 15715 }, +] + [[package]] name = "anyio" version = "4.8.0" From 71990326fb00797e9b736759bb563ac133bf0787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Fri, 17 Jan 2025 10:22:52 +0100 Subject: [PATCH 3/9] fixup, implement everything so that test runs to completion --- tests/workbenches/conftest.py | 163 +++++++++++++++++- .../notebook-controller/test_spawning.py | 15 +- 2 files changed, 169 insertions(+), 9 deletions(-) diff --git a/tests/workbenches/conftest.py b/tests/workbenches/conftest.py index 1bc7d5b..b2e7968 100644 --- a/tests/workbenches/conftest.py +++ b/tests/workbenches/conftest.py @@ -1,14 +1,21 @@ from __future__ import annotations +import logging +import time +import traceback +from typing import Callable, Any, 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) -> KubeResourceManager: +def function_resource_manager(admin_client: DynamicClient) -> Generator[KubeResourceManager, None, None]: resource_manager = KubeResourceManager(admin_client) yield resource_manager resource_manager.destroy() @@ -129,3 +136,157 @@ class OdhConstants: # public static final String OLM_SOURCE_NAME = getOdhOrRhoai("OLM_SOURCE_NAME", ODH_OLM_SOURCE_NAME, RHOAI_OLM_SOURCE_NAME); # public static final String OLM_OPERATOR_CHANNEL = getOdhOrRhoai("OLM_OPERATOR_CHANNEL", ODH_OLM_OPERATOR_CHANNEL, RHOAI_OLM_OPERATOR_CHANNEL); # public static final String OLM_UPGRADE_STARTING_OPERATOR_VERSION = getOdhOrRhoai("OLM_UPGRADE_STARTING_OPERATOR_VERSION", ODH_OLM_UPGRADE_STARTING_OPERATOR_VERSION, RHOAI_OLM_UPGRADE_STARTING_OPERATOR_VERSION); + + +class PodUtils: + READINESS_TIMEOUT = 10 * 60 + + # consider using timeout_sampler + @staticmethod + def waitForPodsReady(client: DynamicClient, namespaceName: str, label_selector: str, expectPodsCount: int): + """Wait for all pods in namespace to be ready + :param client: + :param namespaceName: name of the namespace + :param label_selector: + :param expectPodsCount: + """ + # it's a dynamic client with the `resource` parameter already filled in + class ResourceType(kubernetes.dynamic.Resource, kubernetes.dynamic.DynamicClient): + pass + + resource: ResourceType = client.resources.get( + kind=ocp_resources.pod.Pod.kind, + api_version=ocp_resources.pod.Pod.api_version, + ) + + def ready() -> bool: + pods = resource.get(namespace=namespaceName, label_selector=label_selector).items + if not pods and expectPodsCount == 0: + logging.debug("All expected Pods {} in Namespace {} are ready", label_selector, namespaceName); + return True + if not pods: + logging.debug("Pods matching {}/{} are not ready", namespaceName, label_selector); + return False + if len(pods) != expectPodsCount: + logging.debug("Expected Pods {}/{} are not ready", namespaceName, label_selector); + return False + for pod in pods: + if not Readiness.isPodReady(pod) and not Readiness.isPodSucceeded(pod): + logging.debug("Pod is not ready: {}/{}", namespaceName, pod.getMetadata().getName()); + return False + else: + # check all containers in pods are ready + for cs in pod.status.containerStatuses: + if not (cs.ready or cs.state.get('terminated', {}).get('reason', '') == "Completed"): + logging.debug( + f"Container {cs.getName()} of Pod {namespaceName}/{pod.getMetadata().getName()} not ready") + return False + logging.info("Pods matching {}/{} are ready", namespaceName, label_selector) + return True + + Wait.until(f"readiness of all Pods matching {label_selector} in Namespace {namespaceName}", + TestFrameConstants.GLOBAL_POLL_INTERVAL_MEDIUM, PodUtils.READINESS_TIMEOUT, ready) + + +class Wait: + @staticmethod + def until(description: str, pollInterval: float, timeout: float, ready: Callable[[], bool], + onTimeout: Callable | None = None): + """or every poll (happening once each {@code pollIntervalMs}) checks if supplier {@code ready} is true. + # If yes, the wait is closed. Otherwise, waits another {@code pollIntervalMs} and tries again. + # Once the wait timeout (specified by {@code timeoutMs} is reached and supplier wasn't true until that time, + # runs the {@code onTimeout} (f.e. print of logs, showing the actual value that was checked inside {@code ready}), + # and finally throws {@link WaitException}. + # @param description information about on what we are waiting + # @param pollIntervalMs poll interval in milliseconds + # @param timeoutMs timeout specified in milliseconds + # @param ready {@link BooleanSupplier} containing code, which should be executed each poll, + # verifying readiness of the particular thing + # @param onTimeout {@link Runnable} executed once timeout is reached and + # before the {@link WaitException} is thrown.""" + logging.info("Waiting for: {}", description) + deadline = time.monotonic() + timeout + + exceptionMessage: str | None = None + previousExceptionMessage: str | None = None + + # in case we are polling every 1s, we want to print exception after x tries, not on the first try + # for minutes poll interval will 2 be enough + exceptionAppearanceCount: int = 2 if (pollInterval // 60) > 0 else max(timeout // pollInterval // 4, 2) + exceptionCount: int = 0 + newExceptionAppearance: int = 0 + + stackTraceError: str | None = None + + while True: + try: + result: bool = ready() + except Exception as e: + exceptionMessage = str(e) + + exceptionCount += 1 + newExceptionAppearance += 1 + if (exceptionCount == exceptionAppearanceCount + and exceptionMessage is not None + and exceptionMessage == previousExceptionMessage): + logging.info(f"While waiting for: {description} exception occurred: {exceptionMessage}") + # log the stacktrace + stackTraceError = traceback.format_exc() + elif (exceptionMessage is not None + and exceptionMessage != previousExceptionMessage + and newExceptionAppearance == 2): + previousExceptionMessage = exceptionMessage + + result = False + + timeLeft: float = deadline - time.monotonic() + if result: + return + if timeLeft <= 0: + if exceptionCount > 1: + logging.error("Exception waiting for: {}, {}", description, exceptionMessage) + + if stackTraceError is not None: + # printing handled stacktrace + logging.error(stackTraceError) + if onTimeout is not None: + onTimeout() + waitException: WaitException = WaitException(f"Timeout after {timeout} s waiting for {description}") + logging.error(waitException) + raise waitException + + sleepTime: float = min(pollInterval, timeLeft) + time.sleep(sleepTime) + + +class WaitException(Exception): + pass + + +class Readiness: + @staticmethod + def isPodReady(pod) -> bool: + Utils.checkNotNull(pod, "Pod can't be null.") + + condition = ocp_resources.pod.Pod.Condition.READY + status = ocp_resources.pod.Pod.Condition.Status.TRUE + for cond in pod.get("status", {}).get("conditions", []): + if cond["type"] == condition and cond["status"].casefold() == status.casefold(): + return True + return False + + @staticmethod + def isPodSucceeded(pod) -> bool: + Utils.checkNotNull(pod, "Pod can't be null.") + return pod.status is not None and "Succeeded" == pod.status.phase + + +class Utils: + @staticmethod + def checkNotNull(value, message) -> None: + if value is None: + raise ValueError(message) + + +class TestFrameConstants: + GLOBAL_POLL_INTERVAL_MEDIUM = 10 diff --git a/tests/workbenches/notebook-controller/test_spawning.py b/tests/workbenches/notebook-controller/test_spawning.py index 361937b..cd84e88 100644 --- a/tests/workbenches/notebook-controller/test_spawning.py +++ b/tests/workbenches/notebook-controller/test_spawning.py @@ -19,7 +19,8 @@ import yaml from kubernetes.dynamic import DynamicClient -from tests.workbenches.conftest import OdhAnnotationsLabels, OdhConstants +from tests.conftest import admin_client +from tests.workbenches.conftest import OdhAnnotationsLabels, OdhConstants, PodUtils from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step @@ -81,7 +82,7 @@ def logger(cls): ), }, ) - def testCreateSimpleNotebook(self, function_resource_manager, unprivileged_client): + 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, @@ -110,12 +111,10 @@ def testCreateSimpleNotebook(self, function_resource_manager, unprivileged_clien 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"): - # LabelSelector lblSelector = new LabelSelectorBuilder() - # .withMatchLabels(Map.of("app", NTB_NAME)) - # .build(); - # - # PodUtils.waitForPodsReady(NTB_NAMESPACE, lblSelector, 1, true, () -> { }); + with allure.step("Wait for Notebook pod readiness"): + + lblSelector: str = f"app={self.NTB_NAME}" + PodUtils.waitForPodsReady(admin_client, self.NTB_NAMESPACE, lblSelector, 1) # From 92744bba3c25891624c4cd2d7b3bdfce3552da79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Fri, 17 Jan 2025 10:30:01 +0100 Subject: [PATCH 4/9] uv lock --- uv.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 01fa7ca..704cada 100644 --- a/uv.lock +++ b/uv.lock @@ -296,7 +296,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -1115,6 +1115,7 @@ name = "opendatahub-tests" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "allure-pytest" }, { name = "grpcio-reflection" }, { name = "ipython" }, { name = "openshift-python-utilities" }, @@ -1144,6 +1145,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "allure-pytest", specifier = ">=2.13.5" }, { name = "grpcio-reflection" }, { name = "ipython", specifier = ">=8.18.1" }, { name = "openshift-python-utilities", specifier = ">=5.0.71" }, From 5a79b41d8c814149cd930a7d1dd009b0385db04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Fri, 17 Jan 2025 10:37:25 +0100 Subject: [PATCH 5/9] fixup precommit secret detection --- tests/workbenches/notebook-controller/test_data/notebook.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/workbenches/notebook-controller/test_data/notebook.yaml b/tests/workbenches/notebook-controller/test_data/notebook.yaml index c43c988..6074cd9 100644 --- a/tests/workbenches/notebook-controller/test_data/notebook.yaml +++ b/tests/workbenches/notebook-controller/test_data/notebook.yaml @@ -139,8 +139,8 @@ spec: - name: oauth-config secret: defaultMode: 420 - secretName: my-workbench-oauth-config + secretName: my-workbench-oauth-config # pragma: allowlist secret - name: tls-certificates secret: defaultMode: 420 - secretName: my-workbench-tls + secretName: my-workbench-tls # pragma: allowlist secret From 67271a2e42e9339278b6c1a820dd9d80ae0c1cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Fri, 17 Jan 2025 10:40:05 +0100 Subject: [PATCH 6/9] fixup precommit add types --- tests/workbenches/docs.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/workbenches/docs.py b/tests/workbenches/docs.py index d84c964..5fcab3e 100644 --- a/tests/workbenches/docs.py +++ b/tests/workbenches/docs.py @@ -1,32 +1,38 @@ -def Desc(value: str): +from typing import Callable +import typing + +T = typing.TypeVar("T") + + +def Desc(value: str) -> str: return value def Step( - value: str, - expected: str, -): + value: str, + expected: str, +) -> tuple[str, str]: return value, expected def SuiteDoc( - description: str, - beforeTestSteps: set[Step], - afterTestSteps: set[Step], -): + description: str, + beforeTestSteps: set[Step], + afterTestSteps: set[Step], +) -> Callable[[T], T]: return lambda x: x def Contact( - name: str, - email: str, -): + name: str, + email: str, +) -> tuple[str, str]: return name, email def TestDoc( - description: str, - contact: str, - steps: set[Step], -): + description: str, + contact: str, + steps: set[Step], +) -> Callable[[T], T]: return lambda x: x From 004d870b7c94c3f9275436f413d56e6e1bbe7f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Fri, 17 Jan 2025 10:43:09 +0100 Subject: [PATCH 7/9] fixup precommit reformat --- tests/workbenches/conftest.py | 79 +++++++++++-------- tests/workbenches/docs.py | 20 ++--- .../notebook-controller/test_spawning.py | 1 - 3 files changed, 58 insertions(+), 42 deletions(-) diff --git a/tests/workbenches/conftest.py b/tests/workbenches/conftest.py index b2e7968..8061380 100644 --- a/tests/workbenches/conftest.py +++ b/tests/workbenches/conftest.py @@ -118,10 +118,12 @@ class OdhConstants: # 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) + 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); @@ -150,6 +152,7 @@ def waitForPodsReady(client: DynamicClient, namespaceName: str, label_selector: :param label_selector: :param expectPodsCount: """ + # it's a dynamic client with the `resource` parameter already filled in class ResourceType(kubernetes.dynamic.Resource, kubernetes.dynamic.DynamicClient): pass @@ -162,48 +165,58 @@ class ResourceType(kubernetes.dynamic.Resource, kubernetes.dynamic.DynamicClient def ready() -> bool: pods = resource.get(namespace=namespaceName, label_selector=label_selector).items if not pods and expectPodsCount == 0: - logging.debug("All expected Pods {} in Namespace {} are ready", label_selector, namespaceName); + logging.debug("All expected Pods {} in Namespace {} are ready", label_selector, namespaceName) return True if not pods: - logging.debug("Pods matching {}/{} are not ready", namespaceName, label_selector); + logging.debug("Pods matching {}/{} are not ready", namespaceName, label_selector) return False if len(pods) != expectPodsCount: - logging.debug("Expected Pods {}/{} are not ready", namespaceName, label_selector); + logging.debug("Expected Pods {}/{} are not ready", namespaceName, label_selector) return False for pod in pods: if not Readiness.isPodReady(pod) and not Readiness.isPodSucceeded(pod): - logging.debug("Pod is not ready: {}/{}", namespaceName, pod.getMetadata().getName()); + logging.debug("Pod is not ready: {}/{}", namespaceName, pod.getMetadata().getName()) return False else: # check all containers in pods are ready for cs in pod.status.containerStatuses: - if not (cs.ready or cs.state.get('terminated', {}).get('reason', '') == "Completed"): + if not (cs.ready or cs.state.get("terminated", {}).get("reason", "") == "Completed"): logging.debug( - f"Container {cs.getName()} of Pod {namespaceName}/{pod.getMetadata().getName()} not ready") + f"Container {cs.getName()} of Pod {namespaceName}/{pod.getMetadata().getName()} not ready" + ) return False logging.info("Pods matching {}/{} are ready", namespaceName, label_selector) return True - Wait.until(f"readiness of all Pods matching {label_selector} in Namespace {namespaceName}", - TestFrameConstants.GLOBAL_POLL_INTERVAL_MEDIUM, PodUtils.READINESS_TIMEOUT, ready) + Wait.until( + f"readiness of all Pods matching {label_selector} in Namespace {namespaceName}", + TestFrameConstants.GLOBAL_POLL_INTERVAL_MEDIUM, + PodUtils.READINESS_TIMEOUT, + ready, + ) class Wait: @staticmethod - def until(description: str, pollInterval: float, timeout: float, ready: Callable[[], bool], - onTimeout: Callable | None = None): + def until( + description: str, + pollInterval: float, + timeout: float, + ready: Callable[[], bool], + onTimeout: Callable | None = None, + ): """or every poll (happening once each {@code pollIntervalMs}) checks if supplier {@code ready} is true. - # If yes, the wait is closed. Otherwise, waits another {@code pollIntervalMs} and tries again. - # Once the wait timeout (specified by {@code timeoutMs} is reached and supplier wasn't true until that time, - # runs the {@code onTimeout} (f.e. print of logs, showing the actual value that was checked inside {@code ready}), - # and finally throws {@link WaitException}. - # @param description information about on what we are waiting - # @param pollIntervalMs poll interval in milliseconds - # @param timeoutMs timeout specified in milliseconds - # @param ready {@link BooleanSupplier} containing code, which should be executed each poll, - # verifying readiness of the particular thing - # @param onTimeout {@link Runnable} executed once timeout is reached and - # before the {@link WaitException} is thrown.""" + # If yes, the wait is closed. Otherwise, waits another {@code pollIntervalMs} and tries again. + # Once the wait timeout (specified by {@code timeoutMs} is reached and supplier wasn't true until that time, + # runs the {@code onTimeout} (f.e. print of logs, showing the actual value that was checked inside {@code ready}), + # and finally throws {@link WaitException}. + # @param description information about on what we are waiting + # @param pollIntervalMs poll interval in milliseconds + # @param timeoutMs timeout specified in milliseconds + # @param ready {@link BooleanSupplier} containing code, which should be executed each poll, + # verifying readiness of the particular thing + # @param onTimeout {@link Runnable} executed once timeout is reached and + # before the {@link WaitException} is thrown.""" logging.info("Waiting for: {}", description) deadline = time.monotonic() + timeout @@ -226,15 +239,19 @@ def until(description: str, pollInterval: float, timeout: float, ready: Callable exceptionCount += 1 newExceptionAppearance += 1 - if (exceptionCount == exceptionAppearanceCount - and exceptionMessage is not None - and exceptionMessage == previousExceptionMessage): + if ( + exceptionCount == exceptionAppearanceCount + and exceptionMessage is not None + and exceptionMessage == previousExceptionMessage + ): logging.info(f"While waiting for: {description} exception occurred: {exceptionMessage}") # log the stacktrace stackTraceError = traceback.format_exc() - elif (exceptionMessage is not None - and exceptionMessage != previousExceptionMessage - and newExceptionAppearance == 2): + elif ( + exceptionMessage is not None + and exceptionMessage != previousExceptionMessage + and newExceptionAppearance == 2 + ): previousExceptionMessage = exceptionMessage result = False diff --git a/tests/workbenches/docs.py b/tests/workbenches/docs.py index 5fcab3e..8cfbf40 100644 --- a/tests/workbenches/docs.py +++ b/tests/workbenches/docs.py @@ -9,30 +9,30 @@ def Desc(value: str) -> str: def Step( - value: str, - expected: str, + value: str, + expected: str, ) -> tuple[str, str]: return value, expected def SuiteDoc( - description: str, - beforeTestSteps: set[Step], - afterTestSteps: set[Step], + description: str, + beforeTestSteps: set[Step], + afterTestSteps: set[Step], ) -> Callable[[T], T]: return lambda x: x def Contact( - name: str, - email: str, + name: str, + email: str, ) -> tuple[str, str]: return name, email def TestDoc( - description: str, - contact: str, - steps: set[Step], + description: str, + contact: str, + steps: set[Step], ) -> Callable[[T], T]: return lambda x: x diff --git a/tests/workbenches/notebook-controller/test_spawning.py b/tests/workbenches/notebook-controller/test_spawning.py index cd84e88..ab0806a 100644 --- a/tests/workbenches/notebook-controller/test_spawning.py +++ b/tests/workbenches/notebook-controller/test_spawning.py @@ -112,7 +112,6 @@ def testCreateSimpleNotebook(self, function_resource_manager, admin_client, unpr 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) From c1eddd833fae42f180d9210b9a71cd4d624d3c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= Date: Fri, 17 Jan 2025 10:49:29 +0100 Subject: [PATCH 8/9] fixup precommit types --- tests/workbenches/docs.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/workbenches/docs.py b/tests/workbenches/docs.py index 8cfbf40..fbb85de 100644 --- a/tests/workbenches/docs.py +++ b/tests/workbenches/docs.py @@ -2,6 +2,7 @@ import typing T = typing.TypeVar("T") +StepRType = tuple[str, str] def Desc(value: str) -> str: @@ -11,14 +12,14 @@ def Desc(value: str) -> str: def Step( value: str, expected: str, -) -> tuple[str, str]: +) -> StepRType: return value, expected def SuiteDoc( description: str, - beforeTestSteps: set[Step], - afterTestSteps: set[Step], + beforeTestSteps: set[StepRType], + afterTestSteps: set[StepRType], ) -> Callable[[T], T]: return lambda x: x @@ -33,6 +34,6 @@ def Contact( def TestDoc( description: str, contact: str, - steps: set[Step], + steps: set[StepRType], ) -> Callable[[T], T]: return lambda x: x From 5d73d41e735ecfe9ec2496bc8cc5e6ccb31857cf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:52:04 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/workbenches/conftest.py | 2 +- tests/workbenches/notebook-controller/test_spawning.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/workbenches/conftest.py b/tests/workbenches/conftest.py index 8061380..4646c1a 100644 --- a/tests/workbenches/conftest.py +++ b/tests/workbenches/conftest.py @@ -3,7 +3,7 @@ import logging import time import traceback -from typing import Callable, Any, Generator +from typing import Callable, Generator import kubernetes.dynamic from kubernetes.dynamic import DynamicClient diff --git a/tests/workbenches/notebook-controller/test_spawning.py b/tests/workbenches/notebook-controller/test_spawning.py index ab0806a..681a18a 100644 --- a/tests/workbenches/notebook-controller/test_spawning.py +++ b/tests/workbenches/notebook-controller/test_spawning.py @@ -16,10 +16,8 @@ import ocp_resources.resource import kubernetes.client -import yaml from kubernetes.dynamic import DynamicClient -from tests.conftest import admin_client from tests.workbenches.conftest import OdhAnnotationsLabels, OdhConstants, PodUtils from tests.workbenches.docs import TestDoc, SuiteDoc, Contact, Desc, Step