fix(proxy): keep a failed prisma generate from failing the migration entrypoint (#37947)

The standalone migration entrypoint re-runs `prisma generate` after the
migration completes. That refresh writes into the installed prisma package in
site-packages, which an arbitrary non-root uid cannot do, and which no uid can
do under a read-only root filesystem. Both are supported configurations of the
migrations Job: helm/litellm-helm/tests/migrations-job_tests.yaml asserts
runAsNonRoot, runAsUser and readOnlyRootFilesystem all render.

The write has always failed there, but the failure used to be swallowed. Making
migration failures fatal turned it into a hard exit 1, so a Job that applied
every migration correctly now reports Failed and blocks the rollout it was
supposed to gate.

The refresh is redundant in the shipped images: every Dockerfile generates the
client at build time from the same baked schema, copies it into the runtime
stage, and asserts it resolves there. It stays load-bearing only for a source
checkout, where CircleCI runs the entrypoint under `set +e` and ignores the exit
code anyway. So the call stays and only its exit code stops propagating;
migration failures are still fatal.

image-scan never ran on the change that introduced this, because its path filter
did not list the entrypoint it exercises. Add prisma_migration.py and
entrypoint.sh so the non-root offline migration test gates them from now on.
This commit is contained in:
yuneng-jiang 2026-08-22 11:45:19 -07:00 • committed by GitHub
parent abf99e37d6
commit deab3676e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 24 additions and 23 deletions

View file

@ -17,6 +17,8 @@ on:
- backend/Dockerfile
- backend/main.py
- docker/component_entrypoint.sh
- docker/entrypoint.sh
- litellm/proxy/prisma_migration.py
- litellm-proxy-extras/**
- tests/proxy_migration_tests/**
- uv.lock

View file

@ -1,8 +1,9 @@
"""Standalone entrypoint for applying database migrations and generating the Prisma client.
The entrypoint enforces migration failures by default. Set
ENFORCE_PRISMA_MIGRATION_CHECK=false to preserve log-only behavior for migration and
Prisma generate failures.
Migration failures fail the entrypoint by default; set ENFORCE_PRISMA_MIGRATION_CHECK=false
for log-only behavior. A failed 'prisma generate' is always log-only: every shipped image
bakes the client at build time, and refreshing it writes into site-packages, which an
arbitrary non-root uid or a read-only root filesystem cannot do.
"""
import os
@ -30,13 +31,13 @@ def main() -> int:
verbose_proxy_logger.info("Running 'prisma generate'...")
result: Final = subprocess.run(("prisma", "generate"), capture_output=True, text=True)
verbose_proxy_logger.info("'prisma generate' stdout: %s", result.stdout)
exit_code: Final = result.returncode
if exit_code != 0:
verbose_proxy_logger.info("'prisma generate' failed with exit code %s.", exit_code)
verbose_proxy_logger.error("'prisma generate' stderr: %s", result.stderr)
if enforce_prisma_migration_check:
return exit_code
if result.returncode != 0:
verbose_proxy_logger.warning(
"'prisma generate' exited %s; continuing with the client baked at image build time. stderr: %s",
result.returncode,
result.stderr,
)
return 0

View file

@ -34,24 +34,22 @@ class TestPrismaMigration:
mock_run_server.assert_called_once_with(("--skip_server_startup",), standalone_mode=False)
@pytest.mark.parametrize("env", [{}, {"ENFORCE_PRISMA_MIGRATION_CHECK": "false"}])
@patch("litellm.proxy.prisma_migration.subprocess.run")
@patch("litellm.proxy.prisma_migration.run_server")
def test_main_returns_prisma_generate_exit_code_when_enforced(
self, mock_run_server: MagicMock, mock_subprocess_run: MagicMock
def test_main_exits_zero_when_only_prisma_generate_fails(
self,
mock_run_server: MagicMock,
mock_subprocess_run: MagicMock,
env: dict[str, str],
) -> None:
mock_subprocess_run.return_value = MagicMock(returncode=7, stdout="", stderr="")
mock_subprocess_run.return_value = MagicMock(
returncode=1,
stdout="",
stderr="PermissionError: [Errno 13] Permission denied: '/app/.venv/lib/python3.13/site-packages/prisma/schema.prisma'",
)
with patch.dict(os.environ, {}, clear=True):
assert prisma_migration.main() == 7
@patch("litellm.proxy.prisma_migration.subprocess.run")
@patch("litellm.proxy.prisma_migration.run_server")
def test_main_ignores_prisma_generate_exit_code_when_disabled(
self, mock_run_server: MagicMock, mock_subprocess_run: MagicMock
) -> None:
mock_subprocess_run.return_value = MagicMock(returncode=7, stdout="", stderr="")
with patch.dict(os.environ, {"ENFORCE_PRISMA_MIGRATION_CHECK": "false"}, clear=True):
with patch.dict(os.environ, env, clear=True):
assert prisma_migration.main() == 0
@patch("litellm.proxy.prisma_migration.subprocess.run")