mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Merge 7d696eeb95 into f445e466b4
This commit is contained in:
commit
853c743c06
2 changed files with 80 additions and 11 deletions
|
|
@ -1,4 +1,4 @@
|
|||
"""Image-level regression net for arbitrary-uid boot of the UI image.
|
||||
"""Image-level regression net for the standalone UI image.
|
||||
|
||||
OpenShift ``restricted-v2`` ignores the image ``USER`` and assigns an
|
||||
arbitrary uid in GID 0. The stock nginx base expects to start as root, so
|
||||
|
|
@ -12,6 +12,11 @@ filesystem and ``/tmp`` as the only writable mount, is what catches the
|
|||
whole class: a boot as the default (root) uid passes even on the broken
|
||||
config.
|
||||
|
||||
The Next.js client requests RSC payloads under the dashboard's ``/ui`` URL
|
||||
prefix, while the static export stores them at the nginx document root. The
|
||||
image must strip that prefix for existing payloads without serving HTML for a
|
||||
missing payload.
|
||||
|
||||
Gated on LITELLM_IMAGE so it is skipped in the normal unit-test run and
|
||||
exercised only where an image has been built (the image-scan workflow).
|
||||
Requires a working docker CLI.
|
||||
|
|
@ -65,6 +70,7 @@ def ui_container() -> Iterator[tuple[str, str]]:
|
|||
"--read-only", "--tmpfs", "/tmp",
|
||||
IMAGE,
|
||||
)
|
||||
_wait_for_ui(network, container)
|
||||
yield network, container
|
||||
finally:
|
||||
_docker("logs", container, check=False)
|
||||
|
|
@ -96,17 +102,30 @@ def _probe(network: str, container: str, path: str) -> "subprocess.CompletedProc
|
|||
)
|
||||
|
||||
|
||||
def test_ui_serves_as_arbitrary_uid_read_only(ui_container: tuple[str, str]) -> None:
|
||||
"""nginx boots and serves as an arbitrary uid with a read-only root fs.
|
||||
def _fetch_body(network: str, container: str, path: str) -> "subprocess.CompletedProcess[str]":
|
||||
"""Fetch a payload body and make curl fail on an HTTP error response."""
|
||||
return _docker(
|
||||
"run", "--rm", "--network", network, CURL_IMAGE,
|
||||
"--silent", "--show-error", "--fail", "--max-time", "10",
|
||||
f"http://{container}:{UI_PORT}{path}",
|
||||
check=False,
|
||||
)
|
||||
|
||||
On the pre-fix config nginx exits during startup with
|
||||
``mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)``
|
||||
and the running-check below fails; it never reaches the probes.
|
||||
"""
|
||||
network, container = ui_container
|
||||
|
||||
def _probe_content_type(network: str, container: str, path: str) -> "subprocess.CompletedProcess[str]":
|
||||
"""Read the response MIME type without retaining another payload body."""
|
||||
return _docker(
|
||||
"run", "--rm", "--network", network, CURL_IMAGE,
|
||||
"--silent", "--show-error", "--fail", "--max-time", "10",
|
||||
"--output", "/dev/null", "--write-out", "%{content_type}",
|
||||
f"http://{container}:{UI_PORT}{path}",
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _wait_for_ui(network: str, container: str) -> None:
|
||||
"""Wait for nginx to serve traffic or fail with its startup logs."""
|
||||
deadline = time.time() + STARTUP_TIMEOUT_SECONDS
|
||||
healthz = None
|
||||
while time.time() < deadline:
|
||||
if not _is_running(container):
|
||||
pytest.fail(
|
||||
|
|
@ -116,17 +135,63 @@ def test_ui_serves_as_arbitrary_uid_read_only(ui_container: tuple[str, str]) ->
|
|||
)
|
||||
healthz = _probe(network, container, "/healthz")
|
||||
if healthz.returncode == 0 and healthz.stdout.strip() == "200":
|
||||
break
|
||||
return
|
||||
time.sleep(2)
|
||||
|
||||
assert healthz is not None and healthz.stdout.strip() == "200", (
|
||||
pytest.fail(
|
||||
f"/healthz never answered 200 within {STARTUP_TIMEOUT_SECONDS}s as uid "
|
||||
f"{ARBITRARY_UID}.\n{_container_logs(container)}"
|
||||
)
|
||||
|
||||
|
||||
def test_ui_serves_as_arbitrary_uid_read_only(ui_container: tuple[str, str]) -> None:
|
||||
"""nginx boots and serves as an arbitrary uid with a read-only root fs.
|
||||
|
||||
On the pre-fix config nginx exits during startup with
|
||||
``mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)``
|
||||
and the fixture fails before it reaches these route probes.
|
||||
"""
|
||||
network, container = ui_container
|
||||
|
||||
for path in ("/", "/ui", "/ui/login"):
|
||||
page = _probe(network, container, path)
|
||||
assert page.stdout.strip() == "200", (
|
||||
f"GET {path} returned {page.stdout.strip()!r} as uid {ARBITRARY_UID}.\n"
|
||||
f"{_container_logs(container)}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("canonical_path", "ui_path"),
|
||||
(
|
||||
("/index.txt", "/ui/index.txt"),
|
||||
("/api-keys/index.txt", "/ui/api-keys/index.txt"),
|
||||
("/api-keys/index.txt", "/ui/api-keys.txt"),
|
||||
("/__next._tree.txt", "/ui/__next._tree.txt"),
|
||||
("/api-keys/__next._tree.txt", "/ui/api-keys/__next._tree.txt"),
|
||||
),
|
||||
ids=("dashboard-root", "trailing-slash-route", "route", "root-tree", "nested-tree"),
|
||||
)
|
||||
def test_ui_prefixed_rsc_payload_matches_static_export(
|
||||
ui_container: tuple[str, str], canonical_path: str, ui_path: str,
|
||||
) -> None:
|
||||
network, container = ui_container
|
||||
|
||||
canonical_payload = _fetch_body(network, container, canonical_path)
|
||||
ui_payload = _fetch_body(network, container, f"{ui_path}?_rsc=regression")
|
||||
ui_content_type = _probe_content_type(network, container, f"{ui_path}?_rsc=regression")
|
||||
|
||||
assert canonical_payload.returncode == 0, canonical_payload.stderr
|
||||
assert ui_payload.returncode == 0, ui_payload.stderr
|
||||
assert ui_content_type.returncode == 0, ui_content_type.stderr
|
||||
assert canonical_payload.stdout
|
||||
assert ui_payload.stdout == canonical_payload.stdout
|
||||
assert ui_content_type.stdout.strip().startswith("text/plain")
|
||||
|
||||
|
||||
def test_ui_prefixed_missing_rsc_payload_stays_404(ui_container: tuple[str, str]) -> None:
|
||||
network, container = ui_container
|
||||
|
||||
missing_payload = _probe(network, container, "/ui/does-not-exist.txt?_rsc=regression")
|
||||
|
||||
assert missing_payload.stdout.strip() == "404", _container_logs(container)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ http {
|
|||
# /ui/<page>.txt would otherwise be rewritten to HTML and break RSC
|
||||
# for nested routes. A genuinely missing payload must 404 (the
|
||||
# router degrades to a hard navigation); never fall back to HTML.
|
||||
location ~ ^/ui/(.+)\.txt$ {
|
||||
try_files /$1.txt /$1/index.txt =404;
|
||||
}
|
||||
|
||||
location ~ \.txt$ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue