docker: read PGADMIN_CONFIG_CONFIG_DATABASE_URI safely via os.environ (fixes #9984)#10009
docker: read PGADMIN_CONFIG_CONFIG_DATABASE_URI safely via os.environ (fixes #9984)#10009asheshv wants to merge 1 commit into
Conversation
…viron Closes pgadmin-org#9984. The entrypoint constructed a Python `-c` argument by shell-substituting `${PGADMIN_CONFIG_CONFIG_DATABASE_URI}` inside a double-quoted Python string: val = check_external_config_db("${PGADMIN_CONFIG_CONFIG_DATABASE_URI}") That collided with the long-standing config_distro.py convention, which requires the env var's value to BE a Python literal (so it produces valid Python when the entrypoint appends `CONFIG_DATABASE_URI = $val` to config_distro.py). Users therefore set the env var to `'postgresql+psycopg://...'` — and the entrypoint then re-wrapped the literal in another set of quotes, producing `check_external_config_db("'postgresql+psycopg://...'")` — a string with literal single quotes inside, which SQLAlchemy cannot parse: sqlalchemy.exc.ArgumentError: Could not parse SQLAlchemy URL from given URL string The Python crash made `$(...)` capture an empty string, so the downstream check at `[ "${external_config_db_exists}" = "False" ]` also failed — silently skipping the entire first-launch user-setup block. That is the second symptom on the issue: PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD getting ignored. This was a regression from commit 1fe840f (Oct 2024, docker secrets support), which incidentally added `"..."` shell quoting around the expansion in an otherwise reasonable PR. The original 2024-09 form (bare `${VAR}` expansion) relied on the Python-literal convention and worked correctly. Rather than restore the bare-expansion form (which is a 7-year Chesterton's fence that the next shellcheck-style cleanup would reintroduce), remove the shell from the quoting question altogether: - Read the env var inside Python via `os.environ` — the shell no longer participates in Python-literal handling. - Use `ast.literal_eval` to unwrap the legacy `'url'` / `"url"` form when present; on `ValueError`/`SyntaxError` fall through to the raw value so users who set the env var without quotes also work. - Default `external_config_db_exists` stays "False" on any Python failure (only overwritten when the Python call produces non-empty stdout). This fixes the secondary PGADMIN_DEFAULT_EMAIL-ignored regression without any other change. Manual verification: - All three env-var forms (`'url'`, `"url"`, raw `url`) now parse to the same clean URI and round-trip through check_external_config_db correctly. - Both branches verified against a local Postgres 17 — returns False when no `server` table exists, True after seeding one.
WalkthroughThe Docker entrypoint's external configuration database existence check is refactored to pass the database URI via environment variable instead of as an inline shell argument. The Python snippet now reads the URI from ChangesDatabase Configuration Check
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pkg/docker/entrypoint.sh (1)
197-220: Significant improvement in correctness and safety.The refactored approach successfully addresses the double-quoting regression from commit 1fe840f and implements a robust safe-fallback strategy. Key improvements:
- Eliminates shell-quoting issues: Reading via
os.environprevents shell expansion and double-quoting of Python literals.- Backward compatible:
ast.literal_evalunwraps legacy'url'forms while supporting raw values via the fallback.- Safe default on failure: Initializing to
"False"and conditionally updating only on success ensures first-launch setup is never silently skipped (#9984).Trade-off: The
2>/dev/nullstderr suppression prioritizes safety over observability—errors are silently absorbed to preserve the safe default. This is intentional per the PR objectives, but may complicate debugging if the check fails unexpectedly in production. Consider whether operational monitoring or logging could detect repeated check failures without compromising the safety guarantee.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/docker/entrypoint.sh` around lines 197 - 220, The current stderr suppression (the "2>/dev/null" on the inline Python invocation) hides failures of check_external_config_db and makes debugging harder; change the call that sets result to capture stderr instead of discarding it (e.g., redirect stderr into a variable or pipe it to the system logger with "logger -t pgadmin-external-config") while keeping the existing behavior of initializing external_config_db_exists="False" and only updating it if result is non-empty; update the invocation that uses SU_EXEC and the inline python (the command that currently ends with 2>/dev/null) to preserve the safe default but emit the captured stderr to logs (or logger) when the check returns empty so operators can observe repeated failures without changing the success/failure logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@pkg/docker/entrypoint.sh`:
- Around line 197-220: The current stderr suppression (the "2>/dev/null" on the
inline Python invocation) hides failures of check_external_config_db and makes
debugging harder; change the call that sets result to capture stderr instead of
discarding it (e.g., redirect stderr into a variable or pipe it to the system
logger with "logger -t pgadmin-external-config") while keeping the existing
behavior of initializing external_config_db_exists="False" and only updating it
if result is non-empty; update the invocation that uses SU_EXEC and the inline
python (the command that currently ends with 2>/dev/null) to preserve the safe
default but emit the captured stderr to logs (or logger) when the check returns
empty so operators can observe repeated failures without changing the
success/failure logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3fccd3bb-7292-492f-b695-05c8199b4536
📒 Files selected for processing (1)
pkg/docker/entrypoint.sh
Closes #9984.
Problem
The container entrypoint constructed a Python
-cargument by shell-substituting${PGADMIN_CONFIG_CONFIG_DATABASE_URI}inside a double-quoted Python string:val = check_external_config_db("${PGADMIN_CONFIG_CONFIG_DATABASE_URI}")That collided with the long-standing
config_distro.pyconvention, which requires the env var's value to be a Python literal (so the entrypoint'sCONFIG_DATABASE_URI = $valwrite produces valid Python). Users therefore set the env var to'postgresql+psycopg://...'. The entrypoint then re-wrapped that literal in another set of quotes, producing:— a string with literal single quotes inside. SQLAlchemy can't parse it:
The Python crash made
$(...)capture an empty string, so the downstream check[ "${external_config_db_exists}" = "False" ]also failed — silently skipping the entire first-launch user-setup block. That is the second symptom on the issue:PGADMIN_DEFAULT_EMAILandPGADMIN_DEFAULT_PASSWORDgetting ignored.Regression history
Bisected to commit
1fe840fca("Allow to pass PGADMIN_CONFIG_CONFIG_DATABASE_URI from docker secrets", #5869, Oct 2024), which incidentally added"…"shell quoting around the expansion in a PR whose actual scope was docker-secrets support (file_envhelper). The pre-regression form (ed211a2bb, #7811, Sep 2024) used a bare expansion and worked correctly with the convention.Both env-var forms have been broken in shipped images since Oct 2024:
PGADMIN_CONFIG_CONFIG_DATABASE_URI="'postgresql+psycopg://…'"(with quotes, documented)ArgumentError+ silentPGADMIN_DEFAULT_EMAILskipPGADMIN_CONFIG_CONFIG_DATABASE_URI=postgresql+psycopg://…(without quotes, naive)config_distro.pySyntaxErroron startup → container exitsFix
Rather than restore the bare-expansion form (a 7-year Chesterton's fence that the next shellcheck-style cleanup would reintroduce), remove the shell from the quoting question altogether:
os.environ— the shell no longer participates in Python-literal handling.ast.literal_evalto unwrap the legacy'url'/"url"form when present; onValueError/SyntaxError, fall through to the raw value (so users who set the env var without quotes also work).external_config_db_existsstays"False"on any Python failure (only overwritten when the Python call produces non-empty stdout) — fixes the secondaryPGADMIN_DEFAULT_EMAIL-ignored regression.Verification (manual)
'postgresql+psycopg://…'(legacy single-quoted)ast.literal_evalunwraps → clean URI ✓"postgresql+psycopg://…"(double-quoted)ast.literal_evalunwraps → clean URI ✓postgresql+psycopg://…(raw, no quotes)ast.literal_evalraises → fall through to raw ✓servertableFalse(first-launch path runs) ✓servertable seededTrue(skip first-launch as designed for #7811) ✓external_config_db_existsstays"False"→ first-launch path runs (no silent skip) ✓Notes
ast.literal_evalsafety: parses Python literals only; cannot execute arbitrary code. Available since Python 2.6 (2008); pgAdmin's Docker image usespython:3-alpine(currently 3.12+), so no version-availability concern.Test plan
bash -n pkg/docker/entrypoint.sh) clean.PGADMIN_CONFIG_CONFIG_DATABASE_URI="'postgresql+psycopg://…'"+PGADMIN_DEFAULT_EMAIL+PGADMIN_DEFAULT_PASSWORD, run a fresh container → verify (1) noArgumentErrorin container logs, (2) default admin user is created against the external DB, (3) restart withservertable populated → first-launch is skipped as in Servers are re-imported at every container startup #7811.Summary by CodeRabbit