Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8eada18
chore: migrate from pip-compile to uv for dependency management
blarghmatey Mar 11, 2026
1d5db68
fix: remove AppArmor/codejail hard dependency; make codejail optional
blarghmatey Mar 11, 2026
37100ad
feat: add ContainerGrader for Kubernetes/Docker-based sandboxed grading
blarghmatey Mar 11, 2026
a66b703
feat: add grader base Docker image and container entrypoint
blarghmatey Mar 11, 2026
01c65ba
feat: add Kubernetes deployment manifests and Docker Compose local dev
blarghmatey Mar 11, 2026
18c7151
fix: correct grader path handling in ContainerGrader and entrypoint
blarghmatey Mar 11, 2026
0be0926
fix(tests): skip jailed grader tests when codejail is not installed
blarghmatey Mar 11, 2026
3f7944d
fix: address PR review feedback
blarghmatey Mar 11, 2026
1491b29
refactor: replace path-py with stdlib pathlib
blarghmatey Mar 11, 2026
cbdcf15
feat: add edx-codejail as optional dependency; document container iso…
blarghmatey Mar 11, 2026
193c043
fix: address second round of PR review feedback
blarghmatey Mar 11, 2026
d28da94
refactor: move full grading pipeline into container; add ContainerGra…
blarghmatey Mar 11, 2026
2c3b82f
refactor: replace statsd/newrelic with OpenTelemetry; add 12-factor s…
blarghmatey Mar 17, 2026
22d311c
chore: remove planning doc from git tracking
blarghmatey Mar 17, 2026
d524236
chore: remove codecov upload from CI
blarghmatey Mar 17, 2026
16e034b
fix: address PR #14 review feedback
blarghmatey Mar 18, 2026
0495e75
fix: add venv bin to PATH so xqueue-watcher entrypoint resolves
blarghmatey Mar 18, 2026
c247b52
feat: add configure_logging() for 12-factor stdout logging
blarghmatey Mar 18, 2026
2e7ab2d
fix: symlink xqueue-watcher into /usr/local/bin for reliable resolution
blarghmatey Mar 18, 2026
5502d68
feat: add env-based defaults for ContainerGrader configuration
blarghmatey Mar 19, 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
20 changes: 9 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@ jobs:
matrix:
os:
- ubuntu-latest
python-version: ['3.12']
python-version: ['3.12', '3.13']
steps:
- uses: actions/checkout@v4
- name: setup python
uses: actions/setup-python@v5

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ matrix.python-version }}
enable-cache: true

- name: Install requirements and Run Tests
run: make test
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Run Coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Run Tests
run: uv run --python ${{ matrix.python-version }} pytest tests
44 changes: 24 additions & 20 deletions .github/workflows/upgrade-python-requirements.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
name: Upgrade Python Requirements
name: Update Dependencies

on:
schedule:
- cron: "15 15 1/14 * *"
workflow_dispatch:
inputs:
branch:
description: "Target branch against which to create requirements PR"
required: true
default: 'master'

jobs:
call-upgrade-python-requirements-workflow:
uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master
with:
branch: ${{ github.event.inputs.branch || 'master' }}
# optional parameters below; fill in if you'd like github or email notifications
# user_reviewers: ""
# team_reviewers: ""
email_address: "aurora-requirements-update@2u-internal.opsgenie.net"
send_success_notification: true
secrets:
requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }}
requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }}
edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }}
edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }}
update-dependencies:
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Update uv.lock
run: uv lock --upgrade

- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }}
commit-message: "chore: update uv.lock with latest dependency versions"
title: "chore: update dependencies"
body: "Automated dependency update via `uv lock --upgrade`."
branch: "chore/update-dependencies"
delete-branch: true
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ reports/
\#*\#
*.egg-info
.idea/

# uv
.venv/

# Kubernetes secrets — never commit real values
deploy/kubernetes/secret.yaml
Automated code Graders With xqueue-watcher.md
48 changes: 31 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
FROM ubuntu:xenial as openedx
FROM python:3.11-slim AS base

RUN apt update && \
apt install -y git-core language-pack-en apparmor apparmor-utils python python-pip python-dev && \
pip install --upgrade pip setuptools && \
rm -rf /var/lib/apt/lists/*
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8

RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
RUN apt-get update && \
apt-get install -y --no-install-recommends git-core && \
rm -rf /var/lib/apt/lists/*

RUN useradd -m --shell /bin/false app

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /edx/app/xqueue_watcher
COPY requirements /edx/app/xqueue_watcher/requirements
RUN pip install -r requirements/production.txt

CMD python -m xqueue_watcher -d /edx/etc/xqueue_watcher
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project

COPY . /edx/app/xqueue_watcher
RUN uv sync --frozen --no-dev && \
ln -s /edx/app/xqueue_watcher/.venv/bin/xqueue-watcher /usr/local/bin/xqueue-watcher
# Note: the `codejail` optional extra (edx-codejail) is intentionally omitted
# from this image. In the Kubernetes deployment, student code runs inside an
# isolated container (ContainerGrader) — the container boundary provides the
# sandbox via Linux namespaces, cgroups, capability dropping, network isolation,
# and a read-only filesystem. codejail (AppArmor + OS-level user-switching)
# requires host-level AppArmor configuration that is unavailable inside
# Kubernetes pods and adds no meaningful security benefit on top of container
# isolation. Install the `codejail` extra only when running the legacy
# JailedGrader on a bare-metal or VM host with AppArmor configured.

RUN useradd -m --shell /bin/false app
USER app

COPY . /edx/app/xqueue_watcher
CMD ["xqueue-watcher", "-d", "/etc/xqueue-watcher"]

FROM openedx as edx.org
RUN pip install newrelic
CMD newrelic-admin run-program python -m xqueue_watcher -d /edx/etc/xqueue_watcher
FROM base AS edx.org
USER app
CMD ["xqueue-watcher", "-d", "/etc/xqueue-watcher"]
56 changes: 19 additions & 37 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,47 +1,29 @@
NODE_BIN=./node_modules/.bin

help:
@echo ' '
@echo 'Makefile for the xqueue-watcher '
@echo ' '
@echo 'Usage: '
@echo ' make requirements install requirements for local development '
@echo ' make test run python unit-tests '
@echo ' make clean delete generated byte code and coverage reports '
@echo ' '

COMMON_CONSTRAINTS_TXT=requirements/common_constraints.txt
.PHONY: $(COMMON_CONSTRAINTS_TXT)
$(COMMON_CONSTRAINTS_TXT):
wget -O "$(@)" https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt || touch "$(@)"

upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade
upgrade: $(COMMON_CONSTRAINTS_TXT)
## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
pip install -q -r requirements/pip_tools.txt
pip-compile --allow-unsafe --rebuild --upgrade -o requirements/pip.txt requirements/pip.in
pip-compile --upgrade -o requirements/pip_tools.txt requirements/pip_tools.in
pip install -q -r requirements/pip.txt
pip install -q -r requirements/pip_tools.txt
pip-compile --upgrade -o requirements/base.txt requirements/base.in
pip-compile --upgrade -o requirements/production.txt requirements/production.in
pip-compile --upgrade -o requirements/test.txt requirements/test.in
pip-compile --upgrade -o requirements/ci.txt requirements/ci.in
@echo ''
@echo 'Makefile for the xqueue-watcher'
@echo ''
@echo 'Usage:'
@echo ' make requirements sync dev dependencies with uv'
@echo ' make test run python unit-tests'
@echo ' make docker-build build the grader base Docker image'
@echo ' make local-run run locally with docker-compose'
@echo ' make clean delete generated byte code'
@echo ''

requirements:
pip install -qr requirements/production.txt --exists-action w
uv sync

test.requirements:
pip install -q -r requirements/test.txt --exists-action w
test: requirements
uv run pytest --cov=xqueue_watcher --cov-report=xml tests

ci.requirements:
pip install -q -r requirements/ci.txt --exists-action w
docker-build:
docker build -t xqueue-watcher:local .
docker build -t grader-base:local -f grader_support/Dockerfile.base .

test: test.requirements
pytest --cov=xqueue_watcher --cov-report=xml tests
local-run:
docker compose up

clean:
find . -name '*.pyc' -delete

# Targets in a Makefile which do not produce an output file with the same name as the target name
.PHONY: help requirements clean
.PHONY: help requirements test docker-build local-run clean
10 changes: 7 additions & 3 deletions conf.d/600.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
"AUTH": ["lms", "lms"],
"HANDLERS": [
{
"HANDLER": "xqueue_watcher.grader.Grader",
"HANDLER": "xqueue_watcher.containergrader.ContainerGrader",
"KWARGS": {
"grader_root": "../data/6.00x/graders/",
"gradepy": "../data/6.00x/graders/grade.py"
"grader_root": "/graders/",
"image": "grader-base:local",
"backend": "docker",
"cpu_limit": "500m",
"memory_limit": "256Mi",
"timeout": 20
}
}
]
Expand Down
103 changes: 103 additions & 0 deletions deploy/kubernetes/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: xqueue-watcher-config
namespace: xqueue-watcher
labels:
app.kubernetes.io/name: xqueue-watcher
app.kubernetes.io/component: watcher
data:
# Main watcher settings. See xqueue_watcher/settings.py for all keys.
xqwatcher.json: |
{
"POLL_INTERVAL": 1,
"LOGIN_POLL_INTERVAL": 5,
"REQUESTS_TIMEOUT": 5,
"POLL_TIME": 10
}

# Logging configuration
logging.json: |
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"stream": "ext://sys.stdout"
}
},
"root": {
"handlers": ["console"],
"level": "INFO"
}
}

# Example queue config — copy this pattern for each course queue.
# Real configs live in conf.d/ mounted from a separate ConfigMap or Secret.
example-queue.json.sample: |
{
"my-course-queue": {
"SERVER": "http://xqueue:18040",
"CONNECTIONS": 2,
"AUTH": ["xqueue_user", "xqueue_pass"],
"HANDLERS": [
{
"HANDLER": "xqueue_watcher.containergrader.ContainerGrader",
"KWARGS": {
"grader_root": "/graders/my-course/",
"image": "registry.example.com/my-course-grader:latest",
"backend": "kubernetes",
"namespace": "xqueue-watcher",
"cpu_limit": "500m",
"memory_limit": "256Mi",
"timeout": 20
}
}
]
}
}
---
# Queue-specific configurations: one JSON file per course queue.
# Operators replace or extend this with real queue names, server URLs,
# and grader images. AUTH credentials should be injected from a Secret
# (e.g., via Vault Secrets Operator) rather than stored in this ConfigMap.
apiVersion: v1
kind: ConfigMap
metadata:
name: xqueue-watcher-queue-configs
namespace: xqueue-watcher
labels:
app.kubernetes.io/name: xqueue-watcher
app.kubernetes.io/component: queue-config
data:
# Replace with your actual queue configs. Each key becomes a file in
# /etc/xqueue-watcher/conf.d/ and must end in .json to be picked up.
example-queue.json: |
{
"my-course-queue": {
"SERVER": "http://xqueue:18040",
"CONNECTIONS": 2,
"AUTH": ["xqueue_user", "xqueue_pass"],
"HANDLERS": [
{
"HANDLER": "xqueue_watcher.containergrader.ContainerGrader",
"KWARGS": {
"grader_root": "/graders/my-course/",
"image": "registry.example.com/my-course-grader:latest",
"backend": "kubernetes",
"namespace": "xqueue-watcher",
"cpu_limit": "500m",
"memory_limit": "256Mi",
"timeout": 20
}
}
]
}
}
Loading
Loading