mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 08:41:46 +08:00
fix: handle read-only filesystem when updating starter project files (#11145)
When running Langflow in containerized environments with readOnlyRootFilesystem: true, the update_project_file() function would fail when trying to write updated project data back to the package installation directory. This fix catches OSError and logs it as debug instead of failing, since the database is the source of truth for project data - file updates are optional convenience for development environments. Fixes #11145
This commit is contained in:
@ -675,10 +675,30 @@ def get_project_data(project):
|
||||
|
||||
|
||||
async def update_project_file(project_path: anyio.Path, project: dict, updated_project_data) -> None:
|
||||
"""Update starter project JSON file with new data.
|
||||
|
||||
This function attempts to write updated project data back to the source file.
|
||||
In containerized environments with read-only filesystems (e.g., Kubernetes with
|
||||
readOnlyRootFilesystem: true), the write will fail gracefully since the database
|
||||
is the source of truth for project data.
|
||||
|
||||
Args:
|
||||
project_path: Path to the project JSON file
|
||||
project: Project dictionary to update
|
||||
updated_project_data: New project data to write
|
||||
"""
|
||||
project["data"] = updated_project_data
|
||||
async with aiofiles.open(str(project_path), "w", encoding="utf-8") as f:
|
||||
await f.write(orjson.dumps(project, option=ORJSON_OPTIONS).decode())
|
||||
await logger.adebug(f"Updated starter project {project['name']} file")
|
||||
try:
|
||||
async with aiofiles.open(str(project_path), "w", encoding="utf-8") as f:
|
||||
await f.write(orjson.dumps(project, option=ORJSON_OPTIONS).decode())
|
||||
await logger.adebug(f"Updated starter project {project['name']} file")
|
||||
except OSError as e:
|
||||
# Handle read-only filesystem (common in containerized environments)
|
||||
# The database update is the important part - file updates are optional
|
||||
await logger.adebug(
|
||||
f"Could not update starter project file {project['name']} (read-only filesystem): {e}. "
|
||||
"This is expected in containerized environments with read-only root filesystem."
|
||||
)
|
||||
|
||||
|
||||
def update_existing_project(
|
||||
|
||||
@ -714,3 +714,77 @@ def test_update_projects_resolves_parser_via_component_type_alias():
|
||||
updated_project = update_projects_components_with_latest_component_versions(project_data, all_types_dict)
|
||||
updated_code = updated_project["nodes"][0]["data"]["node"]["template"]["code"]["value"]
|
||||
assert updated_code == "new_parser_code_v2"
|
||||
|
||||
|
||||
# ==================== Update Project File Tests ====================
|
||||
|
||||
|
||||
async def test_update_project_file_success():
|
||||
"""Test that update_project_file successfully writes to a writable path."""
|
||||
from langflow.initial_setup.setup import update_project_file
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
project_path = Path(temp_dir) / "test_project.json"
|
||||
project = {"name": "Test Project", "data": {"old": "data"}}
|
||||
updated_data = {"new": "data"}
|
||||
|
||||
await update_project_file(project_path, project, updated_data)
|
||||
|
||||
# Verify the file was written
|
||||
assert await project_path.exists()
|
||||
content = await project_path.read_text(encoding="utf-8")
|
||||
import orjson
|
||||
|
||||
written_project = orjson.loads(content)
|
||||
assert written_project["data"] == updated_data
|
||||
assert written_project["name"] == "Test Project"
|
||||
|
||||
|
||||
async def test_update_project_file_readonly_filesystem():
|
||||
"""Test that update_project_file handles read-only filesystem gracefully."""
|
||||
from langflow.initial_setup.setup import update_project_file
|
||||
|
||||
project_path = Path("/nonexistent/readonly/path/test_project.json")
|
||||
project = {"name": "Test Project", "data": {"old": "data"}}
|
||||
updated_data = {"new": "data"}
|
||||
|
||||
# This should NOT raise an exception - it should handle the error gracefully
|
||||
await update_project_file(project_path, project, updated_data)
|
||||
|
||||
# Verify the project dict was still updated (in-memory)
|
||||
assert project["data"] == updated_data
|
||||
|
||||
|
||||
async def test_update_project_file_permission_denied():
|
||||
"""Test that update_project_file handles permission denied gracefully."""
|
||||
from langflow.initial_setup.setup import update_project_file
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
project_path = Path(temp_dir) / "test_project.json"
|
||||
project = {"name": "Test Project", "data": {"old": "data"}}
|
||||
updated_data = {"new": "data"}
|
||||
|
||||
# Mock aiofiles.open to raise OSError (permission denied)
|
||||
with patch("langflow.initial_setup.setup.aiofiles.open") as mock_open:
|
||||
mock_open.side_effect = OSError(13, "Permission denied")
|
||||
|
||||
# Should not raise
|
||||
await update_project_file(project_path, project, updated_data)
|
||||
|
||||
# Verify the project dict was still updated (in-memory)
|
||||
assert project["data"] == updated_data
|
||||
|
||||
|
||||
async def test_update_project_file_logs_debug_on_oserror():
|
||||
"""Test that update_project_file logs a debug message on OSError."""
|
||||
from langflow.initial_setup.setup import update_project_file
|
||||
|
||||
project_path = Path("/nonexistent/readonly/path/test_project.json")
|
||||
project = {"name": "Test Project", "data": {"old": "data"}}
|
||||
updated_data = {"new": "data"}
|
||||
|
||||
with patch("langflow.initial_setup.setup.logger") as mock_logger:
|
||||
mock_logger.adebug = AsyncMock()
|
||||
await update_project_file(project_path, project, updated_data)
|
||||
# Verify debug log was called (either success or error path)
|
||||
assert mock_logger.adebug.called
|
||||
|
||||
Reference in New Issue
Block a user