Skip to content

Commit 321ffd7

Browse files
authored
Merge pull request #17180 from BerriAI/litellm_non_root_docker_logo_fix
[Fix] Add User Writable Directory to Non Root Docker for Logo
2 parents e2e35c3 + 1d95595 commit 321ffd7

File tree

3 files changed

+153
-7
lines changed

3 files changed

+153
-7
lines changed

docker/Dockerfile.non_root

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ RUN cd /app/ui/litellm-dashboard && npm install --legacy-peer-deps
4949
RUN cd /app/ui/litellm-dashboard && npm run build
5050

5151
RUN cp -r /app/ui/litellm-dashboard/out/* /tmp/litellm_ui/
52+
RUN mkdir -p /tmp/litellm_assets && cp /app/litellm/proxy/logo.jpg /tmp/litellm_assets/logo.jpg
5253

5354
RUN cd /tmp/litellm_ui && \
5455
for html_file in *.html; do \
@@ -89,6 +90,7 @@ COPY --from=builder /app/schema.prisma /app/schema.prisma
8990
COPY --from=builder /app/dist/*.whl .
9091
COPY --from=builder /wheels/ /wheels/
9192
COPY --from=builder /tmp/litellm_ui /tmp/litellm_ui
93+
COPY --from=builder /tmp/litellm_assets /tmp/litellm_assets
9294

9395
# Install package from wheel and dependencies
9496
RUN pip install *.whl /wheels/* --no-index --find-links=/wheels/ \
@@ -117,8 +119,8 @@ RUN pip install --no-cache-dir prisma && \
117119
chmod +x docker/prod_entrypoint.sh
118120

119121
# Create directories and set permissions for non-root user
120-
RUN mkdir -p /nonexistent /.npm && \
121-
chown -R nobody:nogroup /app /tmp/litellm_ui /nonexistent /.npm && \
122+
RUN mkdir -p /nonexistent /.npm /tmp/litellm_assets && \
123+
chown -R nobody:nogroup /app /tmp/litellm_ui /tmp/litellm_assets /nonexistent /.npm && \
122124
PRISMA_PATH=$(python -c "import os, prisma; print(os.path.dirname(prisma.__file__))") && \
123125
chown -R nobody:nogroup $PRISMA_PATH && \
124126
LITELLM_PKG_MIGRATIONS_PATH="$(python -c 'import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))' 2>/dev/null || echo '')/migrations" && \
@@ -127,11 +129,11 @@ RUN mkdir -p /nonexistent /.npm && \
127129
# OpenShift compatibility
128130
RUN PRISMA_PATH=$(python -c "import os, prisma; print(os.path.dirname(prisma.__file__))") && \
129131
LITELLM_PROXY_EXTRAS_PATH=$(python -c "import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))" 2>/dev/null || echo "") && \
130-
chgrp -R 0 $PRISMA_PATH /tmp/litellm_ui && \
132+
chgrp -R 0 $PRISMA_PATH /tmp/litellm_ui /tmp/litellm_assets && \
131133
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chgrp -R 0 $LITELLM_PROXY_EXTRAS_PATH || true && \
132-
chmod -R g=u $PRISMA_PATH /tmp/litellm_ui && \
134+
chmod -R g=u $PRISMA_PATH /tmp/litellm_ui /tmp/litellm_assets && \
133135
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g=u $LITELLM_PROXY_EXTRAS_PATH || true && \
134-
chmod -R g+w $PRISMA_PATH /tmp/litellm_ui && \
136+
chmod -R g+w $PRISMA_PATH /tmp/litellm_ui /tmp/litellm_assets && \
135137
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g+w $LITELLM_PROXY_EXTRAS_PATH || true
136138

137139
# Switch to non-root user

litellm/proxy/proxy_server.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8609,7 +8609,19 @@ def get_image():
86098609

86108610
# get current_dir
86118611
current_dir = os.path.dirname(os.path.abspath(__file__))
8612-
default_logo = os.path.join(current_dir, "logo.jpg")
8612+
default_site_logo = os.path.join(current_dir, "logo.jpg")
8613+
8614+
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
8615+
assets_dir = "/tmp/litellm_assets" if is_non_root else current_dir
8616+
8617+
if is_non_root:
8618+
os.makedirs(assets_dir, exist_ok=True)
8619+
8620+
default_logo = (
8621+
os.path.join(assets_dir, "logo.jpg") if is_non_root else default_site_logo
8622+
)
8623+
if is_non_root and not os.path.exists(default_logo):
8624+
default_logo = default_site_logo
86138625

86148626
logo_path = os.getenv("UI_LOGO_PATH", default_logo)
86158627
verbose_proxy_logger.debug("Reading logo from path: %s", logo_path)
@@ -8621,7 +8633,8 @@ def get_image():
86218633
response = client.get(logo_path)
86228634
if response.status_code == 200:
86238635
# Save the image to a local file
8624-
cache_path = os.path.join(current_dir, "cached_logo.jpg")
8636+
cache_dir = assets_dir if is_non_root else current_dir
8637+
cache_path = os.path.join(cache_dir, "cached_logo.jpg")
86258638
with open(cache_path, "wb") as f:
86268639
f.write(response.content)
86278640

tests/test_litellm/proxy/test_proxy_server.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2580,3 +2580,134 @@ def test_get_prompt_spec_for_db_prompt_with_versions():
25802580
prompt_spec_v2 = proxy_config._get_prompt_spec_for_db_prompt(db_prompt=mock_prompt_v2)
25812581
assert prompt_spec_v2.prompt_id == "chat_prompt.v2"
25822582

2583+
2584+
def test_get_image_non_root_uses_tmp_assets_dir(monkeypatch):
2585+
"""
2586+
Test that get_image uses /tmp/litellm_assets when LITELLM_NON_ROOT is true.
2587+
"""
2588+
from unittest.mock import patch
2589+
2590+
from litellm.proxy.proxy_server import get_image
2591+
2592+
# Set LITELLM_NON_ROOT to true
2593+
monkeypatch.setenv("LITELLM_NON_ROOT", "true")
2594+
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
2595+
2596+
# Mock os.path operations
2597+
with patch("litellm.proxy.proxy_server.os.makedirs") as mock_makedirs, \
2598+
patch("litellm.proxy.proxy_server.os.path.exists", return_value=True), \
2599+
patch("litellm.proxy.proxy_server.os.getenv") as mock_getenv, \
2600+
patch("litellm.proxy.proxy_server.FileResponse") as mock_file_response:
2601+
2602+
# Setup mock_getenv to return empty string for UI_LOGO_PATH
2603+
def getenv_side_effect(key, default=""):
2604+
if key == "UI_LOGO_PATH":
2605+
return ""
2606+
elif key == "LITELLM_NON_ROOT":
2607+
return "true"
2608+
return default
2609+
2610+
mock_getenv.side_effect = getenv_side_effect
2611+
2612+
# Call the function
2613+
get_image()
2614+
2615+
# Verify makedirs was called with /tmp/litellm_assets
2616+
mock_makedirs.assert_called_once_with("/tmp/litellm_assets", exist_ok=True)
2617+
2618+
2619+
def test_get_image_non_root_fallback_to_default_logo(monkeypatch):
2620+
"""
2621+
Test that get_image falls back to default_site_logo when logo doesn't exist
2622+
in /tmp/litellm_assets for non-root case.
2623+
"""
2624+
from unittest.mock import patch
2625+
2626+
from litellm.proxy.proxy_server import get_image
2627+
2628+
# Set LITELLM_NON_ROOT to true
2629+
monkeypatch.setenv("LITELLM_NON_ROOT", "true")
2630+
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
2631+
2632+
# Track path.exists calls to verify it checks /tmp/litellm_assets/logo.jpg
2633+
exists_calls = []
2634+
2635+
def exists_side_effect(path):
2636+
exists_calls.append(path)
2637+
# Return False for /tmp/litellm_assets/logo.jpg to trigger fallback
2638+
if "/tmp/litellm_assets/logo.jpg" in path:
2639+
return False
2640+
return True
2641+
2642+
# Mock os.path operations
2643+
with patch("litellm.proxy.proxy_server.os.makedirs") as mock_makedirs, \
2644+
patch("litellm.proxy.proxy_server.os.path.exists", side_effect=exists_side_effect), \
2645+
patch("litellm.proxy.proxy_server.os.getenv") as mock_getenv, \
2646+
patch("litellm.proxy.proxy_server.FileResponse") as mock_file_response:
2647+
2648+
# Setup mock_getenv
2649+
def getenv_side_effect(key, default=""):
2650+
if key == "UI_LOGO_PATH":
2651+
return ""
2652+
elif key == "LITELLM_NON_ROOT":
2653+
return "true"
2654+
return default
2655+
2656+
mock_getenv.side_effect = getenv_side_effect
2657+
2658+
# Call the function
2659+
get_image()
2660+
2661+
# Verify makedirs was called with /tmp/litellm_assets
2662+
mock_makedirs.assert_called_once_with("/tmp/litellm_assets", exist_ok=True)
2663+
2664+
# Verify that exists was called to check /tmp/litellm_assets/logo.jpg
2665+
tmp_logo_path = "/tmp/litellm_assets/logo.jpg"
2666+
assert any(tmp_logo_path in str(call) for call in exists_calls), \
2667+
f"Should check if {tmp_logo_path} exists"
2668+
2669+
# Verify FileResponse was called (with fallback logo)
2670+
assert mock_file_response.called, "FileResponse should be called"
2671+
2672+
2673+
def test_get_image_root_case_uses_current_dir(monkeypatch):
2674+
"""
2675+
Test that get_image uses current_dir when LITELLM_NON_ROOT is not true.
2676+
"""
2677+
from unittest.mock import patch
2678+
2679+
from litellm.proxy.proxy_server import get_image
2680+
2681+
# Don't set LITELLM_NON_ROOT (or set it to false)
2682+
monkeypatch.delenv("LITELLM_NON_ROOT", raising=False)
2683+
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
2684+
2685+
# Mock os.path operations
2686+
with patch("litellm.proxy.proxy_server.os.makedirs") as mock_makedirs, \
2687+
patch("litellm.proxy.proxy_server.os.path.exists", return_value=True), \
2688+
patch("litellm.proxy.proxy_server.os.getenv") as mock_getenv, \
2689+
patch("litellm.proxy.proxy_server.FileResponse") as mock_file_response:
2690+
2691+
# Setup mock_getenv
2692+
def getenv_side_effect(key, default=""):
2693+
if key == "UI_LOGO_PATH":
2694+
return ""
2695+
elif key == "LITELLM_NON_ROOT":
2696+
return "" # Not set or empty
2697+
return default
2698+
2699+
mock_getenv.side_effect = getenv_side_effect
2700+
2701+
# Call the function
2702+
get_image()
2703+
2704+
# Verify makedirs was NOT called with /tmp/litellm_assets (should not create it for root case)
2705+
tmp_assets_calls = [
2706+
call for call in mock_makedirs.call_args_list
2707+
if "/tmp/litellm_assets" in str(call)
2708+
]
2709+
assert len(tmp_assets_calls) == 0, "Should not create /tmp/litellm_assets for root case"
2710+
2711+
# Verify FileResponse was called
2712+
assert mock_file_response.called, "FileResponse should be called"
2713+

0 commit comments

Comments
 (0)