Fix UI disappearing for development instances
This commit is contained in:
parent
ed4a4c13d6
commit
fccd2d1e87
@ -40,7 +40,7 @@ COPY . .
|
||||
ENV LITELLM_NON_ROOT=true
|
||||
|
||||
# Build Admin UI using the upstream command order while keeping a single RUN layer
|
||||
RUN mkdir -p /tmp/litellm_ui && \
|
||||
RUN mkdir -p /var/lib/litellm/ui && \
|
||||
npm install -g npm@latest && npm cache clean --force && \
|
||||
cd /app/ui/litellm-dashboard && \
|
||||
if [ -f "/app/enterprise/enterprise_ui/enterprise_colors.json" ]; then \
|
||||
@ -49,10 +49,10 @@ RUN mkdir -p /tmp/litellm_ui && \
|
||||
rm -f package-lock.json && \
|
||||
npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
cp -r /app/ui/litellm-dashboard/out/* /tmp/litellm_ui/ && \
|
||||
mkdir -p /tmp/litellm_assets && \
|
||||
cp /app/litellm/proxy/logo.jpg /tmp/litellm_assets/logo.jpg && \
|
||||
( cd /tmp/litellm_ui && \
|
||||
cp -r /app/ui/litellm-dashboard/out/* /var/lib/litellm/ui/ && \
|
||||
mkdir -p /var/lib/litellm/assets && \
|
||||
cp /app/litellm/proxy/logo.jpg /var/lib/litellm/assets/logo.jpg && \
|
||||
( cd /var/lib/litellm/ui && \
|
||||
for html_file in *.html; do \
|
||||
if [ "$html_file" != "index.html" ] && [ -f "$html_file" ]; then \
|
||||
folder_name="${html_file%.html}" && \
|
||||
@ -111,8 +111,8 @@ COPY --from=builder /app/docker/entrypoint.sh /app/docker/prod_entrypoint.sh /ap
|
||||
COPY --from=builder /app/docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY --from=builder /app/schema.prisma /app/
|
||||
COPY --from=builder /wheels/ /wheels/
|
||||
COPY --from=builder /tmp/litellm_ui /tmp/litellm_ui
|
||||
COPY --from=builder /tmp/litellm_assets /tmp/litellm_assets
|
||||
COPY --from=builder /var/lib/litellm/ui /var/lib/litellm/ui
|
||||
COPY --from=builder /var/lib/litellm/assets /var/lib/litellm/assets
|
||||
COPY --from=builder /app/.cache /app/.cache
|
||||
COPY --from=builder /app/litellm-proxy-extras /app/litellm-proxy-extras
|
||||
COPY --from=builder \
|
||||
@ -145,8 +145,8 @@ RUN pip install --no-index --find-links=/wheels/ -r requirements.txt && \
|
||||
|
||||
# Permissions, cleanup, and Prisma prep
|
||||
RUN chmod +x docker/entrypoint.sh docker/prod_entrypoint.sh && \
|
||||
mkdir -p /nonexistent /.npm /tmp/litellm_assets /tmp/litellm_ui && \
|
||||
chown -R nobody:nogroup /app /tmp/litellm_ui /tmp/litellm_assets /nonexistent /.npm && \
|
||||
mkdir -p /nonexistent /.npm /var/lib/litellm/assets /var/lib/litellm/ui && \
|
||||
chown -R nobody:nogroup /app /var/lib/litellm/ui /var/lib/litellm/assets /nonexistent /.npm && \
|
||||
pip uninstall jwt -y || true && \
|
||||
pip uninstall PyJWT -y || true && \
|
||||
pip install --no-index --find-links=/wheels/ PyJWT==2.10.1 --no-cache-dir && \
|
||||
@ -156,11 +156,11 @@ RUN chmod +x docker/entrypoint.sh docker/prod_entrypoint.sh && \
|
||||
LITELLM_PKG_MIGRATIONS_PATH="$(python -c 'import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))' 2>/dev/null || echo '')/migrations" && \
|
||||
[ -n "$LITELLM_PKG_MIGRATIONS_PATH" ] && chown -R nobody:nogroup $LITELLM_PKG_MIGRATIONS_PATH && \
|
||||
LITELLM_PROXY_EXTRAS_PATH=$(python -c "import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))" 2>/dev/null || echo "") && \
|
||||
chgrp -R 0 $PRISMA_PATH /tmp/litellm_ui /tmp/litellm_assets && \
|
||||
chgrp -R 0 $PRISMA_PATH /var/lib/litellm/ui /var/lib/litellm/assets && \
|
||||
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chgrp -R 0 $LITELLM_PROXY_EXTRAS_PATH || true && \
|
||||
chmod -R g=u $PRISMA_PATH /tmp/litellm_ui /tmp/litellm_assets && \
|
||||
chmod -R g=u $PRISMA_PATH /var/lib/litellm/ui /var/lib/litellm/assets && \
|
||||
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g=u $LITELLM_PROXY_EXTRAS_PATH || true && \
|
||||
chmod -R g+w $PRISMA_PATH /tmp/litellm_ui /tmp/litellm_assets && \
|
||||
chmod -R g+w $PRISMA_PATH /var/lib/litellm/ui /var/lib/litellm/assets && \
|
||||
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g+w $LITELLM_PROXY_EXTRAS_PATH || true && \
|
||||
chmod -R g+rX $PRISMA_PATH && \
|
||||
chmod -R g+rX /app/.cache && \
|
||||
|
||||
@ -946,20 +946,19 @@ try:
|
||||
# This prevents mutating the packaged UI directory (e.g. site-packages or the repo checkout)
|
||||
# and ensures extensionless routes like /ui/login work via <route>/index.html.
|
||||
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
|
||||
runtime_ui_path = "/tmp/litellm_ui"
|
||||
|
||||
if _dir_has_content(runtime_ui_path):
|
||||
if is_non_root:
|
||||
# Only use runtime UI path in Docker/non-root environments
|
||||
# In local development, use the packaged UI directly
|
||||
if is_non_root:
|
||||
# Use /var/lib/litellm/ui for Docker (more secure than /tmp)
|
||||
runtime_ui_path = "/var/lib/litellm/ui"
|
||||
|
||||
if _dir_has_content(runtime_ui_path):
|
||||
verbose_proxy_logger.info(
|
||||
f"Using pre-built UI for non-root Docker: {runtime_ui_path}"
|
||||
)
|
||||
ui_path = runtime_ui_path
|
||||
else:
|
||||
verbose_proxy_logger.info(
|
||||
f"Using cached runtime UI directory: {runtime_ui_path}"
|
||||
)
|
||||
ui_path = runtime_ui_path
|
||||
else:
|
||||
if is_non_root:
|
||||
verbose_proxy_logger.error(
|
||||
f"UI not found at {runtime_ui_path}. Attempting to populate it from packaged UI."
|
||||
)
|
||||
@ -967,32 +966,32 @@ try:
|
||||
f"Path exists: {os.path.exists(runtime_ui_path)}, Has content: {_dir_has_content(runtime_ui_path)}"
|
||||
)
|
||||
|
||||
try:
|
||||
os.makedirs(runtime_ui_path, exist_ok=True)
|
||||
if not _dir_has_content(runtime_ui_path) and _dir_has_content(
|
||||
packaged_ui_path
|
||||
):
|
||||
shutil.copytree(
|
||||
packaged_ui_path,
|
||||
runtime_ui_path,
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
except Exception as e:
|
||||
if is_non_root:
|
||||
try:
|
||||
os.makedirs(runtime_ui_path, exist_ok=True)
|
||||
if not _dir_has_content(runtime_ui_path) and _dir_has_content(
|
||||
packaged_ui_path
|
||||
):
|
||||
shutil.copytree(
|
||||
packaged_ui_path,
|
||||
runtime_ui_path,
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(
|
||||
f"Failed to populate runtime UI directory {runtime_ui_path} from {packaged_ui_path}: {e}"
|
||||
)
|
||||
else:
|
||||
if _dir_has_content(runtime_ui_path):
|
||||
if is_non_root:
|
||||
else:
|
||||
if _dir_has_content(runtime_ui_path):
|
||||
verbose_proxy_logger.info(
|
||||
f"Using populated UI for non-root Docker: {runtime_ui_path}"
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.info(
|
||||
f"Using populated runtime UI directory: {runtime_ui_path}"
|
||||
)
|
||||
ui_path = runtime_ui_path
|
||||
ui_path = runtime_ui_path
|
||||
else:
|
||||
# Local development: use packaged UI directly, no runtime copy needed
|
||||
verbose_proxy_logger.info(
|
||||
f"Using packaged UI directory for local development: {packaged_ui_path}"
|
||||
)
|
||||
ui_path = packaged_ui_path
|
||||
|
||||
# Only modify files if a custom server root path is set
|
||||
if server_root_path and server_root_path != "/":
|
||||
@ -8885,7 +8884,7 @@ def get_image():
|
||||
default_site_logo = os.path.join(current_dir, "logo.jpg")
|
||||
|
||||
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
|
||||
assets_dir = "/tmp/litellm_assets" if is_non_root else current_dir
|
||||
assets_dir = "/var/lib/litellm/assets" if is_non_root else current_dir
|
||||
|
||||
if is_non_root:
|
||||
os.makedirs(assets_dir, exist_ok=True)
|
||||
|
||||
@ -273,6 +273,10 @@ def test_sso_key_generate_shows_deprecation_banner(client_no_auth, monkeypatch):
|
||||
|
||||
|
||||
def test_restructure_ui_html_files_handles_nested_routes(tmp_path):
|
||||
"""
|
||||
Test that _restructure_ui_html_files correctly restructures HTML files.
|
||||
Note: This function is only called when is_non_root is True (ui_path != packaged_ui_path).
|
||||
"""
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
ui_root = tmp_path / "ui"
|
||||
@ -306,7 +310,10 @@ def test_restructure_ui_html_files_handles_nested_routes(tmp_path):
|
||||
|
||||
|
||||
def test_ui_extensionless_route_requires_restructure(tmp_path):
|
||||
"""Regression for non-root fallback: /ui/login expects login/index.html."""
|
||||
"""
|
||||
Regression for non-root fallback: /ui/login expects login/index.html.
|
||||
Note: Restructuring only happens when is_non_root is True (ui_path != packaged_ui_path).
|
||||
"""
|
||||
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
@ -331,6 +338,49 @@ def test_ui_extensionless_route_requires_restructure(tmp_path):
|
||||
assert "login" in response.text
|
||||
|
||||
|
||||
def test_restructure_only_happens_when_non_root(monkeypatch):
|
||||
"""
|
||||
Test that restructuring logic only executes when LITELLM_NON_ROOT is true.
|
||||
When is_non_root is False, ui_path == packaged_ui_path, so restructuring is skipped.
|
||||
"""
|
||||
# Test Case 1: is_non_root is True - ui_path != packaged_ui_path, so restructuring should happen
|
||||
monkeypatch.setenv("LITELLM_NON_ROOT", "true")
|
||||
|
||||
runtime_ui_path = "/var/lib/litellm/ui"
|
||||
packaged_ui_path = "/some/packaged/ui/path"
|
||||
|
||||
# Simulate the logic from proxy_server.py
|
||||
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
|
||||
if is_non_root:
|
||||
ui_path = runtime_ui_path
|
||||
else:
|
||||
ui_path = packaged_ui_path
|
||||
|
||||
# This is the condition that determines if restructuring happens
|
||||
should_restructure = ui_path != packaged_ui_path
|
||||
|
||||
assert is_non_root is True
|
||||
assert should_restructure is True
|
||||
assert ui_path == runtime_ui_path
|
||||
|
||||
# Test Case 2: is_non_root is False - ui_path == packaged_ui_path, so restructuring should NOT happen
|
||||
monkeypatch.delenv("LITELLM_NON_ROOT", raising=False)
|
||||
|
||||
# Simulate the logic from proxy_server.py
|
||||
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
|
||||
if is_non_root:
|
||||
ui_path = runtime_ui_path
|
||||
else:
|
||||
ui_path = packaged_ui_path
|
||||
|
||||
# This is the condition that determines if restructuring happens
|
||||
should_restructure = ui_path != packaged_ui_path
|
||||
|
||||
assert is_non_root is False
|
||||
assert should_restructure is False
|
||||
assert ui_path == packaged_ui_path
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_scheduled_jobs_credentials(monkeypatch):
|
||||
"""
|
||||
@ -2856,9 +2906,9 @@ def test_root_redirect_when_docs_url_not_root_and_redirect_url_set(monkeypatch):
|
||||
assert response.headers["location"] == test_redirect_url
|
||||
|
||||
|
||||
def test_get_image_non_root_uses_tmp_assets_dir(monkeypatch):
|
||||
def test_get_image_non_root_uses_var_lib_assets_dir(monkeypatch):
|
||||
"""
|
||||
Test that get_image uses /tmp/litellm_assets when LITELLM_NON_ROOT is true.
|
||||
Test that get_image uses /var/lib/litellm/assets when LITELLM_NON_ROOT is true.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
@ -2887,14 +2937,14 @@ def test_get_image_non_root_uses_tmp_assets_dir(monkeypatch):
|
||||
# Call the function
|
||||
get_image()
|
||||
|
||||
# Verify makedirs was called with /tmp/litellm_assets
|
||||
mock_makedirs.assert_called_once_with("/tmp/litellm_assets", exist_ok=True)
|
||||
# Verify makedirs was called with /var/lib/litellm/assets
|
||||
mock_makedirs.assert_called_once_with("/var/lib/litellm/assets", exist_ok=True)
|
||||
|
||||
|
||||
def test_get_image_non_root_fallback_to_default_logo(monkeypatch):
|
||||
"""
|
||||
Test that get_image falls back to default_site_logo when logo doesn't exist
|
||||
in /tmp/litellm_assets for non-root case.
|
||||
in /var/lib/litellm/assets for non-root case.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
@ -2904,13 +2954,13 @@ def test_get_image_non_root_fallback_to_default_logo(monkeypatch):
|
||||
monkeypatch.setenv("LITELLM_NON_ROOT", "true")
|
||||
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
|
||||
|
||||
# Track path.exists calls to verify it checks /tmp/litellm_assets/logo.jpg
|
||||
# Track path.exists calls to verify it checks /var/lib/litellm/assets/logo.jpg
|
||||
exists_calls = []
|
||||
|
||||
def exists_side_effect(path):
|
||||
exists_calls.append(path)
|
||||
# Return False for /tmp/litellm_assets/logo.jpg to trigger fallback
|
||||
if "/tmp/litellm_assets/logo.jpg" in path:
|
||||
# Return False for /var/lib/litellm/assets/logo.jpg to trigger fallback
|
||||
if "/var/lib/litellm/assets/logo.jpg" in path:
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -2933,13 +2983,13 @@ def test_get_image_non_root_fallback_to_default_logo(monkeypatch):
|
||||
# Call the function
|
||||
get_image()
|
||||
|
||||
# Verify makedirs was called with /tmp/litellm_assets
|
||||
mock_makedirs.assert_called_once_with("/tmp/litellm_assets", exist_ok=True)
|
||||
# Verify makedirs was called with /var/lib/litellm/assets
|
||||
mock_makedirs.assert_called_once_with("/var/lib/litellm/assets", exist_ok=True)
|
||||
|
||||
# Verify that exists was called to check /tmp/litellm_assets/logo.jpg
|
||||
tmp_logo_path = "/tmp/litellm_assets/logo.jpg"
|
||||
assert any(tmp_logo_path in str(call) for call in exists_calls), \
|
||||
f"Should check if {tmp_logo_path} exists"
|
||||
# Verify that exists was called to check /var/lib/litellm/assets/logo.jpg
|
||||
assets_logo_path = "/var/lib/litellm/assets/logo.jpg"
|
||||
assert any(assets_logo_path in str(call) for call in exists_calls), \
|
||||
f"Should check if {assets_logo_path} exists"
|
||||
|
||||
# Verify FileResponse was called (with fallback logo)
|
||||
assert mock_file_response.called, "FileResponse should be called"
|
||||
@ -2976,12 +3026,12 @@ def test_get_image_root_case_uses_current_dir(monkeypatch):
|
||||
# Call the function
|
||||
get_image()
|
||||
|
||||
# Verify makedirs was NOT called with /tmp/litellm_assets (should not create it for root case)
|
||||
tmp_assets_calls = [
|
||||
# Verify makedirs was NOT called with /var/lib/litellm/assets (should not create it for root case)
|
||||
var_lib_assets_calls = [
|
||||
call for call in mock_makedirs.call_args_list
|
||||
if "/tmp/litellm_assets" in str(call)
|
||||
if "/var/lib/litellm/assets" in str(call)
|
||||
]
|
||||
assert len(tmp_assets_calls) == 0, "Should not create /tmp/litellm_assets for root case"
|
||||
assert len(var_lib_assets_calls) == 0, "Should not create /var/lib/litellm/assets for root case"
|
||||
|
||||
# Verify FileResponse was called
|
||||
assert mock_file_response.called, "FileResponse should be called"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user