`_ensure_docker_image_available` (app/main.py) and
`DockerExecutor.check_health` (app/services/executor_docker.py) both
unconditionally appended `:latest` to whatever value
`PYTHON_EXECUTOR_DOCKER_IMAGE` held. When an operator sets that env var
to a digest reference (e.g. `repo@sha256:abc...`) for reproducible
deploys, the resulting `repo@sha256:abc...:latest` is invalid Docker
syntax: image lookup fails, the FastAPI lifespan aborts, and the
container crashloops.
Refactor both call sites to use a new `normalize_image_ref` helper.
The helper returns the reference unchanged when it already carries a
tag (`:` after the last `/`) or a digest (`@`), and only appends
`:latest` for bare repositories. Registry ports are handled correctly
because a `:` before the last `/` is a port separator, not a tag.
Coverage added in tests/integration_tests/test_image_ref.py:
* bare repo / namespaced repo / registry/repo all gain :latest
* tagged refs unchanged at every nesting level
* digest refs unchanged at every nesting level
* registry-with-port variants (port + bare, port + tag, port + digest)
* idempotence
Refs #35
Catches drift between the Python package version (what /health reports
to clients) and the chart version (what's actually deployed). If they
get out of sync, a client gating on /health's version may try to use
features that aren't actually shipped.
Reads kubernetes/code-interpreter/Chart.yaml directly via a small regex
so the test has no extra dependencies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The helm chart at kubernetes/code-interpreter/Chart.yaml has been on
0.3.3 since #27, but both pyproject.toml files were still reporting
0.1.0. Now that /health surfaces the package version to clients, line
the pyproject versions up with the chart so a single number tracks
across artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clients can now read ``version`` from the ``/health`` payload to gate
calls to functionality that's only available in newer releases. The
value is sourced from the installed package metadata rather than a
hardcoded string, so the FastAPI app and the health response stay in
sync with the version declared in pyproject.toml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces a string-concatenation expression with an f-string so the
Go-template format string can be read in one go. The doubled braces
(``{{{{`` / ``}}}}``) escape to literal ``{{`` / ``}}`` inside the
f-string, matching Docker's template syntax. Output is byte-identical
to the previous concatenation.
Addresses review feedback on PR #31.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sessions now carry an explicit TTL and are guaranteed to be torn down at
or before that deadline even if the API service crashes.
* CreateSessionRequest gains ``ttl_seconds`` (default 900s, max 24h);
CreateSessionResponse adds the absolute ``expires_at`` timestamp.
* Docker session containers are launched with ``--rm`` and ``sleep <ttl>``
so the container self-destructs at the deadline regardless of whether
the API service is alive. The TTL also goes onto the container as the
``code-interpreter.expires-at`` label for the reaper.
* Kubernetes session pods set ``activeDeadlineSeconds=ttl`` so kubelet
stops the executor container at the deadline; the deadline timestamp
is also stored as a pod annotation so the reaper knows when to delete.
* A new background reaper task (in main.py's lifespan) runs once at
startup — handling crash recovery for any sessions whose TTL elapsed
while the service was down — and then every 30s thereafter.
* Both backends implement ``reap_expired_sessions`` (label-filtered
list followed by per-session deletion) so the reaper is backend-agnostic.
Tests cover TTL bounds at the route layer, the active_deadline /
expires-at metadata on each backend, and reaper behavior under happy
path, missing annotation, invalid annotation, list failure, and partial
delete-failure scenarios.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new routes:
* POST /v1/sessions creates a long-lived executor pod/container,
optionally pre-staging files from /v1/files.
* DELETE /v1/sessions/{id} tears the session down.
Both Docker and Kubernetes backends are supported. Sessions are labeled
``app=code-interpreter,component=session`` so they can be enumerated for
debugging, and named ``code-session-<uuid>`` so the delete path can
sanity-check that callers aren't accidentally targeting other resources.
Known limitation: this PR has no automatic cleanup. A session that the
caller forgets to DELETE will live for the configured idle window
(``SESSION_MAX_LIFETIME_SECONDS`` = 24h). The follow-up PR adds per-session
TTL enforcement plus a reaper for crash recovery.
Tests cover both backends and the route layer (success, 404 mapping,
501 mapping, file resolution, prefix-based safety check on delete).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure refactor - no behavior change. Existing test suite passes unmodified.
* `DockerExecutor._build_run_command` now takes `sleep_seconds` and an
optional `labels` mapping, so future callers can spin up containers with
custom durations and label sets.
* `DockerExecutor._create_tar_archive`'s `code` argument is now optional,
letting callers stage files without an entrypoint.
* `_upload_tar_to_container` extracted from `_stage_files_in_container`
for reuse.
* `KubernetesExecutor._create_pod_manifest` is parameterized over
`command`, `labels`, `annotations`, and `active_deadline_seconds`.
* `KubernetesExecutor._create_tar_archive` mirrors the Docker change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>