Make secret-file reading tolerate a missing file at the default path

A concurrent commit changed DB_PASSWORD_FILE/ENCRYPTION_KEY_FILE's
defaults from "" (opt-in) to fixed /run/secrets/... paths, so production
containers pick them up with zero extra config. But model_post_init reads
these unconditionally at Settings() construction for every process that
imports app.config — including local dev and the test suite, which don't
have that file — so it started crashing the entire test suite with
FileNotFoundError. A missing file now falls back to leaving DATABASE_URL/
ENCRYPTION_KEY untouched instead of crashing; an actual I/O error reading
an existing file still propagates. Added a regression test for exactly
this scenario. 171 backend tests passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Le
2026-07-04 12:45:21 +07:00
parent 7ed53050b7
commit b96139d66f
2 changed files with 44 additions and 9 deletions
+25 -9
View File
@@ -4,9 +4,22 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy.engine import make_url
def _read_secret_file(path: str) -> str:
with open(path, "r") as f:
return f.read().strip()
def _read_secret_file(path: str) -> str | None:
"""Read a Docker-secret file, tolerating a missing file.
DB_PASSWORD_FILE/ENCRYPTION_KEY_FILE default to fixed
`/run/secrets/...` paths so production containers (which mount the
secret there) pick it up with zero extra config — but that same
default runs in every process that imports app.config, including
local dev and the test suite, which don't have that file. A missing
file just means "no override" rather than a hard crash; a real I/O
error reading an existing file still propagates.
"""
try:
with open(path, "r") as f:
return f.read().strip()
except FileNotFoundError:
return None
class Settings(BaseSettings):
@@ -60,13 +73,16 @@ class Settings(BaseSettings):
def model_post_init(self, __context) -> None:
if self.DB_PASSWORD_FILE:
password = _read_secret_file(self.DB_PASSWORD_FILE)
url = make_url(self.DATABASE_URL).set(password=password)
# SQLAlchemy's default str() masks the password with "***" —
# render_as_string(hide_password=False) is needed to get the
# real, usable connection string back.
self.DATABASE_URL = url.render_as_string(hide_password=False)
if password:
url = make_url(self.DATABASE_URL).set(password=password)
# SQLAlchemy's default str() masks the password with "***" —
# render_as_string(hide_password=False) is needed to get the
# real, usable connection string back.
self.DATABASE_URL = url.render_as_string(hide_password=False)
if self.ENCRYPTION_KEY_FILE:
self.ENCRYPTION_KEY = _read_secret_file(self.ENCRYPTION_KEY_FILE)
key = _read_secret_file(self.ENCRYPTION_KEY_FILE)
if key:
self.ENCRYPTION_KEY = key
settings = Settings()