fix: use Python asyncpg migration runner instead of psql, remove postgresql-client from image
This commit is contained in:
@@ -8,7 +8,6 @@ ENV PYTHONPATH=/app
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
postgresql-client \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -33,17 +33,7 @@ spec:
|
||||
- name: run-migrations
|
||||
image: {{ $root.Values.image.registry }}/{{ $svc.image }}:{{ $root.Values.image.tag }}
|
||||
imagePullPolicy: {{ $root.Values.image.pullPolicy }}
|
||||
command: ["sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
for f in $(ls /app/infra/migrations/*.sql 2>/dev/null | sort); do
|
||||
echo "Applying $(basename $f)..."
|
||||
PGPASSWORD="$POSTGRES_PASSWORD" psql \
|
||||
-h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \
|
||||
-U "$POSTGRES_USER" -d "$POSTGRES_DB" \
|
||||
-f "$f" -v ON_ERROR_STOP=0 2>&1 | tail -1 || true
|
||||
done
|
||||
echo "Migrations complete."
|
||||
command: ["python", "-m", "services.shared.migrate"]
|
||||
securityContext:
|
||||
{{- include "stonks.containerSecurityContext" $root | nindent 12 }}
|
||||
envFrom:
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Database migration runner using asyncpg.
|
||||
|
||||
Applies all SQL migration files from infra/migrations/ in sorted order.
|
||||
Each file is split on semicolons and executed statement-by-statement.
|
||||
Idempotent — migrations use IF NOT EXISTS / CREATE OR REPLACE patterns.
|
||||
|
||||
Usage:
|
||||
python -m services.shared.migrate
|
||||
"""
|
||||
import asyncio
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import asyncpg
|
||||
|
||||
logger = logging.getLogger("migrate")
|
||||
|
||||
|
||||
async def run_migrations() -> None:
|
||||
host = os.getenv("POSTGRES_HOST", "localhost")
|
||||
port = int(os.getenv("POSTGRES_PORT", "5432"))
|
||||
user = os.getenv("POSTGRES_USER", "stonks")
|
||||
password = os.getenv("POSTGRES_PASSWORD", "")
|
||||
database = os.getenv("POSTGRES_DB", "stonks")
|
||||
|
||||
migrations_dir = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "infra", "migrations"
|
||||
)
|
||||
migrations_dir = os.path.normpath(migrations_dir)
|
||||
|
||||
if not os.path.isdir(migrations_dir):
|
||||
logger.error("Migrations directory not found: %s", migrations_dir)
|
||||
sys.exit(1)
|
||||
|
||||
files = sorted(glob.glob(os.path.join(migrations_dir, "*.sql")))
|
||||
if not files:
|
||||
logger.warning("No migration files found in %s", migrations_dir)
|
||||
return
|
||||
|
||||
logger.info("Connecting to %s@%s:%d/%s", user, host, port, database)
|
||||
conn = await asyncpg.connect(
|
||||
host=host, port=port, user=user, password=password, database=database
|
||||
)
|
||||
|
||||
try:
|
||||
for path in files:
|
||||
name = os.path.basename(path)
|
||||
with open(path) as f:
|
||||
sql = f.read()
|
||||
# Split on semicolons and execute each statement individually.
|
||||
# asyncpg.execute() doesn't support multi-statement strings.
|
||||
statements = [s.strip() for s in sql.split(";") if s.strip()]
|
||||
try:
|
||||
for stmt in statements:
|
||||
await conn.execute(stmt)
|
||||
logger.info(" ✓ %s (%d statements)", name, len(statements))
|
||||
except Exception as exc:
|
||||
logger.warning(" ⚠ %s: %s", name, exc)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
logger.info("Migrations complete (%d files)", len(files))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(name)s %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
asyncio.run(run_migrations())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user