A live grading dashboard for the OCP4 Getting Started Workshop. It monitors participant namespaces in real time and displays progress on a gamified leaderboard.
Browser (instructor laptop / projector)
|
Route (edge TLS)
|
Service :8080
|
Pod (Python FastAPI)
|
Kubernetes API (read-only ClusterRole)
|
wksp-user1, wksp-user2, ... wksp-userN namespaces
The grading app runs inside the OpenShift cluster with a ServiceAccount that has read-only access to all user namespaces. It checks for specific Kubernetes objects (Deployments, Routes, Secrets, Pipelines, etc.) that participants create during the workshop and scores their progress.
- Dark "mission control" themed UI designed for projecting in a classroom
- Real-time leaderboard ranked by completion percentage
- Per-module completion bars showing class-wide progress
- Click any student row to expand detailed per-check pass/fail status
- Auto-refresh every 30 seconds (configurable, with toggle)
- Trophy and medal icons for top finishers
- Sortable columns (rank, username, progress)
| Module | Exercise | What's Checked |
|---|---|---|
| 3 | Parksmap Deployment | Deployment parksmap with >= 1 replica |
| 4 | Scaling | Deployment parksmap with >= 2 replicas |
| 5 | Routes | Route parksmap exists |
| 7 | Permissions | RoleBinding granting view to default SA |
| 9 | Nationalparks Backend | Deployment + BuildConfig + labeled Route |
| 10 | Database | MongoDB Deployment + Secret + Service |
| 11 | Health Checks | Readiness & liveness probes on nationalparks |
| 12 | Pipelines | Pipeline + successful PipelineRun + Tasks |
| 13 | Triggers | TriggerTemplate + TriggerBinding + EventListener |
Modules 1-2 (login/projects), 6 (logging), and 8 (remote shell) have no gradeable objects and are skipped.
- OpenShift 4.x cluster with workshop namespaces provisioned (
wksp-user1,wksp-user2, ...) ocCLI authenticated as a user with cluster-admin privileges (needed to create ClusterRole/ClusterRoleBinding)podmanordockerfor building the container image- A container registry (e.g., quay.io) to push the image
# 1. Clone and enter the repo
git clone <repo-url>
cd OCP4StarterWorkshopGrading
# 2. Log into OpenShift as cluster-admin
oc login --token=<token> --server=<api-url>
# 3. Create namespace and RBAC
oc apply -f manifests/namespace.yaml
oc apply -f manifests/serviceaccount.yaml
oc apply -f manifests/clusterrole.yaml
oc apply -f manifests/clusterrolebinding.yaml
# 4. Deploy the ConfigMap (edit modules.yaml section if needed)
oc apply -f manifests/configmap.yaml
# 5. Build and push the container image
podman build -t quay.io/<your-org>/ocp4-workshop-grader:latest -f Containerfile .
podman push quay.io/<your-org>/ocp4-workshop-grader:latest
# 6. Update the image in manifests/deployment.yaml, then deploy
oc apply -f manifests/deployment.yaml
oc apply -f manifests/service.yaml
oc apply -f manifests/route.yaml
# 7. Get the dashboard URL
echo "https://$(oc get route workshop-grader -n workshop-grading -o jsonpath='{.spec.host}')"If you prefer a single command after building and pushing the image:
oc apply -f manifests/OCP4StarterWorkshopGrading/
├── Containerfile # Multi-stage build (Hummingbird Python 3.12, distroless)
├── requirements.txt # Python dependencies
├── README.md
├── app/
│ ├── main.py # FastAPI app and API endpoints
│ ├── config.py # Configuration from env vars + YAML
│ ├── grader.py # Grading orchestration, caching, parallel queries
│ ├── checks.py # Individual grading check functions
│ ├── k8s_client.py # Kubernetes API client wrapper
│ ├── models.py # Pydantic response models
│ └── static/
│ └── index.html # Single-page dashboard (HTML + CSS + JS)
└── manifests/
├── namespace.yaml # workshop-grading namespace
├── serviceaccount.yaml # grader-sa ServiceAccount
├── clusterrole.yaml # Read-only ClusterRole (least privilege)
├── clusterrolebinding.yaml # Binds ClusterRole to grader-sa
├── configmap.yaml # Runtime config + module/check definitions
├── deployment.yaml # App Deployment (1 replica, health probes)
├── service.yaml # ClusterIP Service on port 8080
└── route.yaml # Edge-terminated TLS Route
All configuration is in the grader-config ConfigMap (manifests/configmap.yaml).
| Setting | Default | Description |
|---|---|---|
namespace_prefix |
wksp-user |
Prefix to match user namespaces (matches wksp-user1, wksp-user2, ...) |
cache_ttl_seconds |
30 |
How long to cache K8s API results before re-querying |
refresh_interval_seconds |
30 |
How often the browser auto-refreshes (sent to frontend) |
Each module has a list of checks. Available check types:
| Check Type | Parameters | Description |
|---|---|---|
deployment_exists |
name, min_replicas |
Deployment exists with N available replicas |
deployment_has_probes |
name |
Deployment has readiness + liveness probes |
route_exists |
name, required_labels (optional) |
Route exists, optionally with specific labels |
rolebinding_view_default |
(none) | RoleBinding granting view to default SA |
secret_exists |
name_pattern (regex) |
Secret matching the name pattern |
service_exists |
name |
Service exists |
buildconfig_exists |
name_substring |
BuildConfig with name containing substring |
pipeline_exists |
(none) | At least one Tekton Pipeline |
pipelinerun_succeeded |
(none) | At least one successful PipelineRun |
task_exists |
(none) | At least one Tekton Task |
resource_exists |
group, version, plural |
Generic CRD existence check |
To add, remove, or modify grading checks without rebuilding the image:
oc edit configmap grader-config -n workshop-grading
oc rollout restart deployment workshop-grader -n workshop-gradingFor example, to change the scaling check from 2 replicas to 3, find the module 4 section in the modules.yaml data key and update min_replicas: 3.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Dashboard UI |
GET |
/api/grades |
Full grading results for all users (JSON) |
GET |
/api/grades/{username} |
Single user results (e.g., /api/grades/1 for namespace wksp-user1) |
GET |
/api/config |
Module names and refresh interval (used by frontend) |
GET |
/api/health |
Liveness/readiness probe endpoint |
Run the dashboard locally against a remote OpenShift cluster using your kubeconfig:
# Install dependencies
pip install -r requirements.txt
# Point to your cluster (uses ~/.kube/config by default)
export IN_CLUSTER=false
export NAMESPACE_PREFIX=wksp-user
# Start the dev server with hot reload
uvicorn app.main:app --reload --port 8080Open http://localhost:8080 in your browser.
When running locally without the ConfigMap volume mount, the app uses built-in default module definitions. To customize modules locally, create a standalone modules.yaml file and set MODULES_CONFIG_PATH to point to it:
export MODULES_CONFIG_PATH=./modules.yaml"No user namespaces found"
Check that namespace_prefix in the ConfigMap matches your namespace naming convention. The default is wksp-user, which matches wksp-user1, wksp-user2, etc. If your namespaces use a different convention like lab-user1, lab-user2, set the prefix to lab-user.
403 Forbidden errors in pod logs The ServiceAccount doesn't have the required permissions. Re-apply the ClusterRole and ClusterRoleBinding:
oc apply -f manifests/clusterrole.yaml
oc apply -f manifests/clusterrolebinding.yamlTekton/Pipeline checks all show "not found" The OpenShift Pipelines operator may not be installed on the cluster. Pipeline and Trigger checks gracefully degrade — they report "not found" instead of crashing when the Tekton CRDs don't exist.
Pod won't start (ImagePullBackOff)
Check that the image in manifests/deployment.yaml matches the tag you pushed to your registry. Verify the image is accessible from the cluster.
Dashboard loads but shows stale data
The backend caches results for cache_ttl_seconds (default 30s). If you need immediate updates, you can lower this value in the ConfigMap and restart the pod.