Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
cb7959c
automate notebook examples end-to-end
tmvfb Jun 11, 2026
ca88b4e
Better error handling
tmvfb Jun 11, 2026
ca17604
ci: fix output readability and error reporting
tmvfb Jun 11, 2026
caf78e1
ci: fix KFP v2 polling and prevent Phase 4 crash from aborting run
tmvfb Jun 11, 2026
d629a2b
fix: YAML filename typo and CI report/skip improvements
tmvfb Jun 11, 2026
d2bee85
ci: label KFP poll rows with source notebook name
tmvfb Jun 11, 2026
58f575b
fix: AIGatewayKey owner field, MLflow pre-flight check, ISVC timing
tmvfb Jun 12, 2026
9c04ad4
ci: pipeline error details, ci-skip cell tag mechanism, tag debug sec…
tmvfb Jun 12, 2026
1e5b8ad
fix: dask version pins + KFP task-level error extraction
tmvfb Jun 12, 2026
857dbee
fix: mlflow==3.10.0 (non-existent) → mlflow>=3.0.0,<4 in pipeline com…
tmvfb Jun 12, 2026
aa00d44
revert: restore mlflow==3.10.0 in pipeline components
tmvfb Jun 12, 2026
81d0c5c
fix: upgrade KFP component base images from python:3.9 to python:3.11
tmvfb Jun 12, 2026
a66758f
build: add mobile-price-classification components image v2
tmvfb Jun 12, 2026
407b5f3
fix: upgrade anyio>=4.12 in dask notebook pip install
tmvfb Jun 12, 2026
16322db
fix: pin dask to 2026.3.0 and update worker image tag
tmvfb Jun 12, 2026
bfc6024
ci: add mobile-price-classification, mnist-vae, minimal-container-com…
tmvfb Jun 15, 2026
ebd9da2
ci: make notebooks/mnist-vae opt-in via --include-pytorch
tmvfb Jun 15, 2026
e07aa7f
ci: install pytorch-lightning before mnist-vae training
tmvfb Jun 15, 2026
c03700d
fix: self-install pytorch-lightning in run_training.py
tmvfb Jun 15, 2026
d710ea4
fix: remove multi_class param from LogisticRegression (removed in skl…
tmvfb Jun 15, 2026
6372763
fix: strip @domain from MLflow username when constructing ISVC model …
tmvfb Jun 15, 2026
023c31f
fix: add mlflow-isvc-sa SA to avoid KServe S3 credential injection co…
tmvfb Jun 15, 2026
f2a2015
fix: add mlflow-isvc-sa SA to inference-protocols example
tmvfb Jun 15, 2026
411b023
fix: skip KFP run ID extraction for ISVC-only notebooks in CI
tmvfb Jun 15, 2026
074f62e
fix: resolve libstdc++ CXXABI_1.3.15 error and wire --max_epochs in m…
tmvfb Jun 15, 2026
0e91774
fix: also self-install torchvision when missing in mnist-vae
tmvfb Jun 15, 2026
9adf9a8
fix: self-install tensorboard when missing in mnist-vae
tmvfb Jun 15, 2026
d65e2cc
feat: add CI coverage for hf-vllm-completion, kserve-keda, shadow-dep…
tmvfb Jun 15, 2026
d9a27fe
fix: detect postgres-operator via apply instead of CRD get
tmvfb Jun 16, 2026
6fcfac6
fix: detect KEDA via ScaledObject apply instead of CRD get
tmvfb Jun 16, 2026
e7665fe
fix: surface actionable error messages for postgres-operator failures
tmvfb Jun 16, 2026
dcdfb3e
fix: suppress traceback in apply.py scripts, print clean error message
tmvfb Jun 16, 2026
69ef93b
refactor: break run_all() into phase helpers and add _timed_run wrapper
tmvfb Jun 16, 2026
91d3308
refactor: declarative Example/Step registration table in CI
tmvfb Jun 16, 2026
74a043c
docs: add ci/README.md with conventions for adding examples, scripts,…
tmvfb Jun 16, 2026
62a72ac
docs: add ci/ and scripts/ to repo overview, link to ci/README.md
tmvfb Jun 16, 2026
5556e65
Cosmetics
tmvfb Jun 16, 2026
cd9f21f
docs: align MLflow setup instructions across all MLflow notebooks
tmvfb Jun 16, 2026
bd0f9d5
fix: update MLflow permissions link and add Create Access Token hint
tmvfb Jun 16, 2026
c3a3b05
refactor: extract inference-protocols deploy into apply.py, notebook …
tmvfb Jun 16, 2026
5c81be1
fix: remove hardcoded namespace from ServiceAccount YAMLs
tmvfb Jun 16, 2026
efd4a96
Update GitHub Actions versions in mobile-price-classification workflow
Korel Jun 18, 2026
4229ae6
Add optional --namespace flag to get_or_create_api_key script
Korel Jun 18, 2026
e89ef2c
fix: apply ServiceAccount in inference-protocols apply.py, drop dead …
tmvfb Jun 16, 2026
e130b8b
feat: support older deployments in get_or_create_api_key
tmvfb Jun 18, 2026
b0907e8
fix: detect AIGatewayKey CRD via api-resources, not get crd
tmvfb Jun 18, 2026
463b2ab
docs: add tip about auto-created examples key and manual override in …
tmvfb Jun 18, 2026
86a2f4b
docs: document auto-created examples-key and manual override in servi…
tmvfb Jun 18, 2026
1724100
remove: drop AIGatewayKey CRD / AI Gateway auth, use env var or manua…
tmvfb Aug 5, 2026
49b9e61
feat: add INFERENCE_SERVICE_API_KEY preflight check, skip api-key-dep…
tmvfb Aug 5, 2026
639a279
fix: serialize env-mutating notebooks in Phase 1, retry internal pred…
tmvfb Aug 6, 2026
70ef806
fix: drop unnecessary pandas pin from dask notebook pip install
tmvfb Aug 6, 2026
62bc43f
fix: use <isvc>-predictor internal Service DNS name in smoke tests
tmvfb Aug 6, 2026
88c0268
feat: add kserve_internal_url helper, auto-detect agentgateway vs dir…
tmvfb Aug 6, 2026
8aaa14f
fix: probe agentgateway via DNS instead of kubectl (RBAC forbidden cr…
tmvfb Aug 6, 2026
78216e4
refactor: trim verbose docstrings
tmvfb Aug 6, 2026
b51d925
refactor: package shared utilities as installable pk_helpers
tmvfb Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/mobile-price-classification-components.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Build Mobile Price Classification Components Image

on:
workflow_dispatch:
inputs:
tag:
description: "Image tag (e.g. v2, v3)"
required: true
default: "v2"

env:
DOCKERFILE_PATH: "pipelines/lightweight-python-package/Dockerfile"
BUILD_CONTEXT_DIR: "pipelines/lightweight-python-package/"
IMAGE_NAME: "mobile-price-classification"
IMAGE_PUSH_PATH: "${{ secrets.GCP_ARTIFACT_REGISTRY_PATH }}"
IMAGE_PUSH_SECRET: "${{ secrets.GCP_SA_KEY }}"

jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache

- name: Checkout code
uses: actions/checkout@v7

- name: Auth GCloud CLI
uses: google-github-actions/auth@v3
with:
credentials_json: ${{ env.IMAGE_PUSH_SECRET }}

- name: Docker Login to Google Artifact Registry
uses: docker/login-action@v4
with:
registry: europe-west3-docker.pkg.dev
username: _json_key
password: ${{ env.IMAGE_PUSH_SECRET }}

- name: Build and push image
run: |
TAG=${{ github.event.inputs.tag }}
VERSIONED=${{ env.IMAGE_PUSH_PATH }}/${{ env.IMAGE_NAME }}:${TAG}
LATEST=${{ env.IMAGE_PUSH_PATH }}/${{ env.IMAGE_NAME }}:latest
COMMIT=${{ env.IMAGE_PUSH_PATH }}/${{ env.IMAGE_NAME }}:commit-${{ github.sha }}
BUILD_CONTEXT="${GITHUB_WORKSPACE}/${{ env.BUILD_CONTEXT_DIR }}"
docker build -t $VERSIONED -f ${{ env.DOCKERFILE_PATH }} "${BUILD_CONTEXT}"
docker tag $VERSIONED $LATEST
docker tag $VERSIONED $COMMIT
docker push $VERSIONED
docker push $LATEST
docker push $COMMIT
echo "Pushed: $VERSIONED"
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,25 @@ For full platform documentation, see [docs.prokube.ai](https://docs.prokube.ai/)
```py
.
├── .github # workflows to build images
├── ci # CI orchestrator (run_all.py) and contributor conventions
├── hparam-tuning # hyperparameter tuning examples (Katib)
├── images # custom container images used by examples
├── mlflow # MLflow experiment tracking examples
├── notebooks # Jupyter notebook examples (Dask, MNIST VAE, etc.)
├── pipelines # Kubeflow Pipelines examples
├── rstudio # RStudio examples
├── serving # model serving examples (KServe, vLLM, shadow deployments)
└── src/pk_helpers # installable prokube helpers (credential setup, API key, KServe URLs)
```

The shared `pk_helpers` package is installable from the repo root
(`pip install -e .`); notebooks install it via a `%pip install -q -e ~/kubeflow-examples`
setup cell and import helpers with `from pk_helpers import ...`.

Many serving examples include an `apply.py` (deploy + smoke-test) and a `cleanup.py`
(teardown of Kubernetes resources) alongside the notebook. These are used by the CI
orchestrator and can also be run manually.

## Note about storage
Storage on Kubernetes is a complex topic and a deep dive is outside the scope of this repository. There are two types
of storage you might encounter here — block and object storage.
Expand Down Expand Up @@ -43,6 +53,12 @@ Some examples require a minimum prokube platform version. If an example is not l
|---|---|---|
| `serving/minimal-s3-model` | v1.7.0 | Requires s3creds secret with KServe support |

## CI

Examples are tested end-to-end by `ci/run_all.py`, which runs inside a Kubeflow
notebook pod. See [`ci/README.md`](ci/README.md) for how the orchestrator works and
how to add a new example to CI.

## Contributing
All code contributions should go via pull requests. Make sure your code is clearly documented and that it adheres
to established standards (e.g. PEP).
Expand Down
233 changes: 233 additions & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# CI — contributor guide

> This document is written primarily for AI agents making changes to this
> repository. It describes the conventions that must be followed for a new
> example to be picked up by `ci/run_all.py` correctly.

---

## Adding a new example

Register it in the `_EXAMPLES` list in `run_all.py`. Everything else —
phase scheduling, opt-in gating, cleanup, dry-run output, MLflow-credential
and API-key skipping, env-mutating ordering — is derived from that entry
automatically.

```python
Example(
name="serving/my-new-example", # display name and result key
steps=[
Step("script", "serving/my-new-example/apply.py"),
# or: Step("notebook", "serving/my-new-example/example.ipynb")
# chain multiple steps if needed (executed sequentially)
],
phase=1, # see Phase rules below
cleanup="serving/my-new-example/cleanup.py", # omit if no K8s resources
opt_in="include_foo", # omit if always enabled
mlflow_dependent=False, # True = skip when MLflow creds absent
api_key_dependent=False, # True = skip when INFERENCE_SERVICE_API_KEY is unset
env_mutating=False, # True = pip installs/upgrades packages;
# runs before the rest of its phase (see below)
)
```

### env_mutating flag

Every notebook in a phase is executed via papermill against the **same**
`python3` kernel/site-packages — there is no per-notebook virtualenv. If a
notebook runs `pip install --upgrade <pkg>` while another notebook in the
same phase is concurrently importing that package, the concurrent
reinstall can corrupt the import (e.g. `ModuleNotFoundError:
No module named 'pandas._libs.internals'` from a partially-replaced
compiled extension).

Set `env_mutating=True` on any example whose steps run `pip install` /
`%pip install` (uncommented, not `-q`-only-metadata, actually mutating
installed packages) against packages that other examples in the same
phase might import. `run_all.py` runs all `env_mutating` examples in a
phase to completion **before** starting the rest of that phase, instead of
throwing everything into the same parallel batch. Prefer avoiding
`pip install --upgrade` in new examples entirely (pin/bake deps into the
notebook image) — reach for `env_mutating=True` only when that isn't
possible.

### Phase rules

| Phase | When to use |
|-------|-------------|
| 1 | Self-contained: does not depend on anything else in CI |
| 2 | Submits a KFP pipeline and returns fast; actual run is polled in Phase 4 |
| 3 | Requires a model already registered by the Phase 2 mlflow-mobile-price pipeline |

### Opt-in flag

Add `opt_in="include_foo"` and a corresponding `--include-foo` argument in
the `__main__` block of `run_all.py`. Use opt-in when the example requires
cluster add-ons (KEDA, postgres-operator, GPU nodes) that are not guaranteed
to be present.

---

## apply.py and cleanup.py

### cleanup.py — always add when K8s resources are created

Any example that creates Kubernetes resources (InferenceService, Deployment,
Service, CRD instance, …) must have a `cleanup.py` in its directory.
CI runs all cleanup scripts in parallel in a `finally` block so they execute
even on failure.

A cleanup script must:
- Delete resources idempotently (`--ignore-not-found`)
- Never raise on failure (print a warning at most)
- Read the namespace from the pod filesystem:

```python
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f:
ns = f.read().strip()
```

**Do not** add a `cleanup.py` for examples that only create in-cluster
transient objects managed by KFP (pipeline runs, artifacts) — those are
cleaned up by KFP's own retention policy.

### apply.py — only for serving examples driven by a script

Add an `apply.py` only when the CI execution of a serving example is driven
by a Python script rather than a notebook. This is the case when:

- The notebook's deploy/test section requires manual inputs that cannot be
automated, so a separate script owns the full deploy-wait-smoketest cycle.
- The example has no notebook at all.

An `apply.py` must exit non-zero on failure (CI relies on the return code).
It must print a clean error message to stderr and suppress the full traceback:

```python
if __name__ == "__main__":
try:
deploy()
except Exception as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)
```

If you add an `apply.py`, always add the matching `cleanup.py` as well.

---

## pk_helpers package

`pk_helpers` (source in `src/pk_helpers/`) bundles the prokube-platform
utilities importable from notebooks and apply scripts. It is a regular,
installable Python package — install it once, editable, from the repo root:

```bash
pip install -e .
```

In notebooks, do this from a setup cell near the top:

```python
%pip install -q -e ~/kubeflow-examples
```

CI installs it automatically in the preflight step (`_ensure_pk_helpers`),
so `apply.py` scripts can `from pk_helpers import ...` without any path
wiring. Every helper works identically whether called from a notebook cell
or from an `apply.py`.

### setup_mlflow_credentials

One-time, interactive. Stores MLflow credentials in the
`mlflow-credentials` K8s secret. **Requires human input** (the MLflow
Personal Access Token cannot be obtained programmatically). Run it once from
a JupyterLab terminal via the installed console script:

```bash
pk-setup-mlflow-credentials
```

Do not call this from CI. CI validates the secret exists in the preflight
check and skips MLflow-dependent examples if it does not.

### get_or_create_api_key

Returns a model-serving API key: the `INFERENCE_SERVICE_API_KEY` env var if
an admin has injected one into the pod, otherwise an interactive prompt
(ask your cluster admin, or use pkui if available on your platform). Call it
from any notebook cell or script that needs an inference API key:

```python
from pk_helpers import get_or_create_api_key

API_KEY = get_or_create_api_key()
```

CI runs headlessly and cannot answer the interactive prompt, so
`INFERENCE_SERVICE_API_KEY` **must** be exported before running
`ci/run_all.py`:

```bash
export INFERENCE_SERVICE_API_KEY=<your-api-key> # ask your admin, or use pkui if available
```

CI validates this in the preflight check and skips `api_key_dependent`
examples if it is unset — mark any new example that calls
`get_or_create_api_key()` with `api_key_dependent=True`.

### internal_predict_url

Returns the correct **internal, in-cluster** predict URL for a KServe
InferenceService, compatible with both prokube generations:

- Currently released: hits the predictor Service directly —
`http://<isvc-name>-predictor.<namespace>.svc.cluster.local/v1/models/<model-name>:predict`.
- Upcoming (agentgateway-based, not yet released): routes through the
shared `agentgateway-proxy` Service instead —
`http://agentgateway-proxy.agentgateway-system.svc.cluster.local/_platform/serving/<namespace>/<isvc-name>/v2/models/<model-name>/infer`.

It picks the URL via a plain DNS lookup for the `agentgateway-proxy` Service
(cached for the process) — no config flag needed, and no RBAC required
(unlike `kubectl get service`, which notebook pod service accounts
typically can't do cross-namespace; DNS resolution needs no permissions,
and a non-existent Service just fails to resolve). The request/response
payload format is unchanged either way (plain KServe V1 JSON, e.g.
`{"instances": [...]}`) — only the URL differs. Any example that predicts
against an InferenceService via its internal cluster URL (not the external
gateway URL) should use this instead of hardcoding the `<isvc>-predictor`
pattern:

```python
from pk_helpers import internal_predict_url

url = internal_predict_url(isvc_name, namespace, model_name)
```

---

## ci-skip cell tag

Tag a notebook cell with `ci-skip` to have CI replace it with a no-op comment
before execution. Use this **only** for cells that genuinely cannot run
headlessly:

- Cells that require manual input (hardcoded paths, paste-your-token
placeholders, `input()` calls).
- Interactive widget cells (`ipywidgets`, `ipykernel` display-only code).

**Do not** use `ci-skip` to work around a fixable automation problem. If a
cell fails because it needs credentials or a namespace, fix it to load those
automatically (see the MLflow credential pattern above). `ci-skip` is a last
resort, not a convenience.

To tag a cell in JupyterLab: select the cell → Property Inspector (right
panel) → add `ci-skip` under Cell Tags.

In raw notebook JSON the tag appears as:

```json
"metadata": {
"tags": ["ci-skip"]
}
```
Loading