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:
DevByteAI
2026-04-09 12:32:50 -07:00
committed by Eric Hare
parent 47ac11290a
commit 387f5f997f
2 changed files with 97 additions and 3 deletions

View File

@ -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(

View File

@ -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