Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: canonical/charmed-kubeflow-uats
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 3bd24e81f1608d84e78657d49e473b6301c84a08
Choose a base ref
..
head repository: canonical/charmed-kubeflow-uats
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 579f41a3207de0606a6061d1ff0db35feb28e431
Choose a head ref
55 changes: 37 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ very least) of the following pieces:
* MicroK8s
* Charmed Kubernetes
* EKS cluster
* AKS cluster <!-- codespell-ignore -->
* **Charmed Kubeflow** deployed on top of it
* **MLFlow (optional)** deployed alongside Kubeflow

@@ -77,26 +78,41 @@ In order to run the tests using the `driver`:
source venv/bin/activate
pip install tox
```
* Run the UATs:

```bash
# assumes an existing `kubeflow` Juju model
tox -e uats
```
Then in order to run UATs, there are two options:

You can also run a subset of the provided tests using the `--filter` option and passing a filter
that follows the same syntax as the pytest `-k` option, e.g.
#### Run tests from a remote commit
In this case, tests are fetched from a remote commit of `charmed-kubeflow-uats` repository. In order to define the commit, tests use the hash of the `HEAD`, where the repository is checked out locally. This means that when you want to run tests from a specific branch, you need to check out to that branch and then run the tests. Note that if the locally checked out commit is not pushed to the remote repository, then tests will fail.

```bash
# run all tests containing 'kfp' or 'katib' in their name
tox -e uats -- --filter "kfp or katib"
# run any test that doesn't contain 'kserve' in its name
tox -e uats -- --filter "not kserve"
```
```bash
# assumes an existing `kubeflow` Juju model
tox -e uats-remote
```

#### Run tests from local copy

This one works only when running the tests from the same node where the tests job is deployed (e.g. running from the same machine where the Microk8s cluster lives). In this case, the tests job instantiates a volume that is [mounted to the local directory of the repository where tests reside](https://github.com/canonical/charmed-kubeflow-uats/blob/ee0fa08931b11f40e97dbe3e340c413cf466a084/assets/test-job.yaml.j2#L34-L36). If unsure about your setup, use the `-remote` option.

```bash
# assumes an existing `kubeflow` Juju model
tox -e uats-local
```

#### Run a subset of UATs

You can also run a subset of the provided tests using the `--filter` option and passing a filter
that follows the same syntax as the pytest `-k` option, e.g.

```bash
# run any test that doesn't contain 'kserve' in its name
tox -e uats-remote -- --filter "not kserve"
# run all tests containing 'kfp' or 'katib' in their name
tox -e uats-local -- --filter "kfp or katib"
```

This simulates the behaviour of running `pytest -k "some filter"` directly on the test suite.
You can read more about the options provided by Pytest in the corresponding section of the
[documentation](https://docs.pytest.org/en/7.4.x/reference/reference.html#command-line-flags).
This simulates the behaviour of running `pytest -k "some filter"` directly on the test suite.
You can read more about the options provided by Pytest in the corresponding section of the
[documentation](https://docs.pytest.org/en/7.4.x/reference/reference.html#command-line-flags).

#### Run Kubeflow UATs

@@ -105,7 +121,10 @@ dedicated `kubeflow` tox test environment:

```bash
# assumes an existing `kubeflow` Juju model
tox -e kubeflow
# run tests from the checked out commit after fetching them remotely
tox -e kubeflow-remote
# run tests from the local copy of the repo
tox -e kubeflow-local
```

#### Developer Notes
@@ -117,7 +136,7 @@ a Kubernetes Job to run the tests. More specifically, the `driver` executes the
1. Create a Kubeflow Profile (i.e. `test-kubeflow`) to run the tests in
2. Submit a Kubernetes Job (i.e. `test-kubeflow`) that runs `tests`
The Job performs the following:
* Mount the local `tests` directory to a Pod that uses `jupyter-scipy` as the container image
* If a `-local` tox environment is run, then it mounts the local `tests` directory to a Pod that uses `jupyter-scipy` as the container image. Else (in `-remote` tox environments), it creates an emptyDir volume which it syncs to the current commit that the repo is checked out locally, using a [git-sync](https://github.com/kubernetes/git-sync/) `initContainer`.
* Install python dependencies specified in the [requirements.txt](tests/requirements.txt)
* Run the test suite by executing `pytest`
3. Wait until the Job completes (regardless of the outcome)
36 changes: 34 additions & 2 deletions assets/test-job.yaml.j2
Original file line number Diff line number Diff line change
@@ -11,16 +11,28 @@ spec:
access-ml-pipeline: "true"
mlflow-server-minio: "true"
spec:
{% if not tests_local_run %}
# securityContext is needed in order for test files to be writeable
# since the tests save the notebooks. Setting it enables:
# * The test-volume to be group-owned by this GID.
# * The GID to be added to each container.
securityContext:
fsGroup: 101
{% endif %}
serviceAccountName: default-editor
containers:
- name: {{ job_name }}
image: {{ test_image }}
image: {{ tests_image }}
command:
- bash
- -c
args:
- |
{% if tests_local_run %}
cd /tests;
{% else %}
cd /tests/charmed-kubeflow-uats/tests;
{% endif %}
pip install -r requirements.txt >/dev/null;
{{ pytest_cmd }};
# Kill Istio Sidecar after workload completes to have the Job status properly updated
@@ -30,8 +42,28 @@ spec:
volumeMounts:
- name: test-volume
mountPath: /tests
{% if not tests_local_run %}
initContainers:
- name: git-sync
# This container pulls git data and publishes it into volume
# "test-volume".
image: registry.k8s.io/git-sync/git-sync:v4.0.0
args:
- --repo=https://github.com/canonical/charmed-kubeflow-uats
- --ref={{ tests_remote_commit }}
- --root=/tests
- --group-write
- --one-time
volumeMounts:
- name: test-volume
mountPath: /tests
{% endif %}
volumes:
- name: test-volume
{% if tests_local_run %}
hostPath:
path: {{ test_dir }}
path: {{ tests_local_dir }}
{% else %}
emptyDir: {}
{% endif %}
restartPolicy: Never
22 changes: 17 additions & 5 deletions driver/test_kubeflow_workloads.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@

import logging
import os
import subprocess
from pathlib import Path

import pytest
@@ -16,7 +17,9 @@
JOB_TEMPLATE_FILE = ASSETS_DIR / "test-job.yaml.j2"
PROFILE_TEMPLATE_FILE = ASSETS_DIR / "test-profile.yaml.j2"

TESTS_DIR = os.path.abspath(Path("tests"))
TESTS_LOCAL_RUN = eval(os.environ.get("LOCAL"))
TESTS_LOCAL_DIR = os.path.abspath(Path("tests"))

TESTS_IMAGE = "kubeflownotebookswg/jupyter-scipy:v1.7.0"

NAMESPACE = "test-kubeflow"
@@ -39,6 +42,13 @@ def pytest_filter(request):
return f"-k '{filter}'" if filter else ""


@pytest.fixture(scope="session")
def tests_checked_out_commit(request):
"""Retrieve active git commit."""
head = subprocess.check_output(["git", "rev-parse", "HEAD"])
return head.decode("UTF-8").rstrip()


@pytest.fixture(scope="session")
def pytest_cmd(pytest_filter):
"""Format the Pytest command."""
@@ -95,16 +105,18 @@ async def test_create_profile(lightkube_client, create_profile):
assert_namespace_active(lightkube_client, NAMESPACE)


def test_kubeflow_workloads(lightkube_client, pytest_cmd):
def test_kubeflow_workloads(lightkube_client, pytest_cmd, tests_checked_out_commit):
"""Run a K8s Job to execute the notebook tests."""
log.info(f"Starting Kubernetes Job {NAMESPACE}/{JOB_NAME} to run notebook tests...")
resources = list(
codecs.load_all_yaml(
JOB_TEMPLATE_FILE.read_text(),
context={
"job_name": JOB_NAME,
"test_dir": TESTS_DIR,
"test_image": TESTS_IMAGE,
"tests_local_run": TESTS_LOCAL_RUN,
"tests_local_dir": TESTS_LOCAL_DIR,
"tests_image": TESTS_IMAGE,
"tests_remote_commit": tests_checked_out_commit,
"pytest_cmd": pytest_cmd,
},
)
@@ -121,7 +133,7 @@ def test_kubeflow_workloads(lightkube_client, pytest_cmd):
)
finally:
log.info("Fetching Job logs...")
fetch_job_logs(JOB_NAME, NAMESPACE)
fetch_job_logs(JOB_NAME, NAMESPACE, TESTS_LOCAL_RUN)


def teardown_module():
8 changes: 7 additions & 1 deletion driver/utils.py
Original file line number Diff line number Diff line change
@@ -79,8 +79,14 @@ def wait_for_job(
raise ValueError(f"Unknown status {job.status} for Job {namespace}/{job_name}!")


def fetch_job_logs(job_name, namespace):
def fetch_job_logs(job_name, namespace, tests_local_run):
"""Fetch the logs produced by a Kubernetes Job."""
if not tests_local_run:
print("##### git-sync initContainer logs #####")
command = ["kubectl", "logs", "-n", namespace, f"job/{job_name}", "-c", "git-sync"]
subprocess.check_call(command)

print("##### test-kubeflow container logs #####")
command = ["kubectl", "logs", "-n", namespace, f"job/{job_name}"]
subprocess.check_call(command)

10 changes: 5 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ google-auth==2.29.0
# via kubernetes
h11==0.14.0
# via httpcore
httpcore==1.0.4
httpcore==1.0.5
# via httpx
httpx==0.27.0
# via lightkube
@@ -63,7 +63,7 @@ jedi==0.19.1
# via ipython
jinja2==3.1.3
# via pytest-operator
juju==3.3.1.1
juju==3.4.0.0
# via
# -r requirements.in
# pytest-operator
@@ -101,18 +101,18 @@ pluggy==1.4.0
# via pytest
prompt-toolkit==3.0.43
# via ipython
protobuf==5.26.0
protobuf==5.26.1
# via macaroonbakery
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.2
# via stack-data
pyasn1==0.5.1
pyasn1==0.6.0
# via
# juju
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
pycparser==2.21
# via cffi
10 changes: 5 additions & 5 deletions tests/notebooks/e2e-wine/requirements.txt
Original file line number Diff line number Diff line change
@@ -14,9 +14,9 @@ attrs==23.2.0
# referencing
blinker==1.7.0
# via flask
boto3==1.34.70
boto3==1.34.73
# via -r requirements.in
botocore==1.34.70
botocore==1.34.73
# via
# boto3
# s3transfer
@@ -211,11 +211,11 @@ pyarrow==10.0.1
# via
# -r requirements.in
# mlflow
pyasn1==0.5.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
pydantic==1.10.14
# via kfp
@@ -317,7 +317,7 @@ threadpoolctl==3.4.0
# via scikit-learn
tqdm==4.66.2
# via shap
typer==0.10.0
typer==0.11.1
# via kfp
typing-extensions==4.10.0
# via
4 changes: 2 additions & 2 deletions tests/notebooks/katib/requirements.txt
Original file line number Diff line number Diff line change
@@ -29,11 +29,11 @@ oauthlib==3.2.2
# requests-oauthlib
protobuf==3.20.3
# via kubeflow-katib
pyasn1==0.5.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
python-dateutil==2.9.0.post0
# via kubernetes
6 changes: 3 additions & 3 deletions tests/notebooks/kfp_v1/requirements.txt
Original file line number Diff line number Diff line change
@@ -97,11 +97,11 @@ protobuf==3.20.3
# kfp
# kfp-pipeline-spec
# proto-plus
pyasn1==0.5.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
pydantic==1.10.14
# via kfp
@@ -151,7 +151,7 @@ tenacity==8.2.3
# via -r requirements.in
termcolor==2.4.0
# via fire
typer==0.10.0
typer==0.11.1
# via kfp
typing-extensions==4.10.0
# via
4 changes: 2 additions & 2 deletions tests/notebooks/kfp_v2/requirements.txt
Original file line number Diff line number Diff line change
@@ -62,11 +62,11 @@ protobuf==4.25.3
# kfp
# kfp-pipeline-spec
# proto-plus
pyasn1==0.5.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
python-dateutil==2.9.0.post0
# via
8 changes: 4 additions & 4 deletions tests/notebooks/kserve/requirements.txt
Original file line number Diff line number Diff line change
@@ -79,7 +79,7 @@ h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.4
httpcore==1.0.5
# via httpx
httptools==0.6.1
# via uvicorn
@@ -123,7 +123,7 @@ opencensus==0.11.4
# via ray
opencensus-context==0.1.3
# via opencensus
orjson==3.9.15
orjson==3.10.0
# via kserve
packaging==24.0
# via
@@ -152,11 +152,11 @@ psutil==5.9.8
# via kserve
py-spy==0.3.14
# via ray
pyasn1==0.5.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
pydantic==1.10.14
# via
12 changes: 6 additions & 6 deletions tests/notebooks/mlflow-kserve/requirements.txt
Original file line number Diff line number Diff line change
@@ -30,9 +30,9 @@ attrs==23.2.0
# referencing
blinker==1.7.0
# via flask
boto3==1.34.70
boto3==1.34.73
# via -r requirements.in
botocore==1.34.70
botocore==1.34.73
# via
# boto3
# s3transfer
@@ -121,7 +121,7 @@ h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.4
httpcore==1.0.5
# via httpx
httptools==0.6.1
# via uvicorn
@@ -217,7 +217,7 @@ opencensus==0.11.4
# via ray
opencensus-context==0.1.3
# via opencensus
orjson==3.9.15
orjson==3.10.0
# via kserve
packaging==22.0
# via
@@ -260,11 +260,11 @@ pyarrow==10.0.1
# via
# -r requirements.in
# mlflow
pyasn1==0.5.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
pydantic==1.10.14
# via
4 changes: 2 additions & 2 deletions tests/notebooks/mlflow/requirements.txt
Original file line number Diff line number Diff line change
@@ -8,9 +8,9 @@ alembic==1.13.1
# via mlflow
blinker==1.7.0
# via flask
boto3==1.34.70
boto3==1.34.73
# via -r requirements.in
botocore==1.34.70
botocore==1.34.73
# via
# boto3
# s3transfer
4 changes: 2 additions & 2 deletions tests/notebooks/training/requirements.txt
Original file line number Diff line number Diff line change
@@ -25,11 +25,11 @@ oauthlib==3.2.2
# via
# kubernetes
# requests-oauthlib
pyasn1==0.5.1
pyasn1==0.6.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
pyasn1-modules==0.4.0
# via google-auth
python-dateutil==2.9.0.post0
# via kubernetes
16 changes: 12 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -52,7 +52,8 @@ commands =
codespell {toxinidir}/. --skip {toxinidir}/./.git --skip {toxinidir}/./.tox \
--skip {toxinidir}/./build --skip {toxinidir}/./lib --skip {toxinidir}/./venv \
--skip {toxinidir}/./.mypy_cache \
--skip {toxinidir}/./icon.svg --skip *.json.tmpl
--skip {toxinidir}/./icon.svg --skip *.json.tmpl \
--ignore-regex=".*codespell-ignore."
# pflake8 wrapper supports config from pyproject.toml
pflake8 {[vars]all_path}
isort --check-only --diff {[vars]all_path}
@@ -61,23 +62,30 @@ deps =
-r requirements-lint.txt
description = Check code against coding style standards

[testenv:kubeflow]
[testenv:kubeflow-{local,remote}]
commands =
# run all tests apart from the ones that use MLFlow
pytest -vv --tb native {[vars]driver_path} -s --filter "not mlflow" --model kubeflow {posargs}
setenv =
local: LOCAL = True
remote: LOCAL = False
deps =
-r requirements.txt
description = Run UATs for Kubeflow

[testenv:uats]
[testenv:uats-{local,remote}]
# provide a filter when calling tox to (de)select test cases based on their names, e.g.
# * run all tests containing 'kfp' or 'katib' in their name:
# $ tox -e uats -- --filter "kfp or katib"
# * run any test that doesn't contain 'kserve' in its name:
# $ tox -e uats -- --filter "not kserve"
# this simulates the behaviour of running 'pytest -k "<filter>"' directly on the test suite:
# https://docs.pytest.org/en/7.4.x/reference/reference.html#command-line-flags
commands = pytest -vv --tb native {[vars]driver_path} -s --model kubeflow {posargs}
commands =
pytest -vv --tb native {[vars]driver_path} -s --model kubeflow {posargs}
setenv =
local: LOCAL = True
remote: LOCAL = False
deps =
-r requirements.txt
description = Run UATs for Kubeflow and Integrations