Files
getpgdata/site/app.py
T

212 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Flask-приложение для доступа к PostgreSQL (managed k8s, internal-only).
Структура соответствует требованиям платформы Nubes:
repo/
└── site/
├── app.py
├── static/
└── templates/
Подключение к БД — через переменные окружения:
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS, DB_SSLMODE
Если БД недоступна — приложение стартует и показывает ошибку на странице,
а не падает (важно для k8s health-проб и диагностики).
Запуск в dev:
python site/app.py
"""
import os
from flask import Flask, abort, redirect, render_template, request, url_for
import psycopg2
import psycopg2.extras
app = Flask(__name__, template_folder="templates", static_folder="static")
def db_config():
"""Возвращает параметры подключения к PostgreSQL из окружения."""
return {
"host": os.getenv("DB_HOST", "postgresqlk8s-master.60bdf3e3-5087-41ff-b760-fe6ea544a80e.svc.cluster.local"),
"port": int(os.getenv("DB_PORT", "5432")),
"dbname": os.getenv("DB_NAME", "postgres"),
"user": os.getenv("DB_USER", "super"),
"password": os.getenv("DB_PASS", ""),
"sslmode": os.getenv("DB_SSLMODE", "disable"),
"connect_timeout": int(os.getenv("DB_CONNECT_TIMEOUT", "5")),
}
def connect():
"""Открывает соединение с PostgreSQL."""
try:
return psycopg2.connect(**db_config())
except psycopg2.Error as exc:
raise ConnectionError(f"Нет соединения с PostgreSQL: {exc}") from exc
@app.route("/healthz")
def healthz():
"""Liveness-проба для k8s."""
try:
with connect() as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1")
cur.fetchone()
return {"status": "ok", "db": "connected"}, 200
except Exception:
return {"status": "degraded", "db": "unreachable"}, 503
@app.route("/")
def index():
"""Главная: состояние подключения + список таблиц."""
cfg = db_config()
show_password = os.getenv("SHOW_DB_PASSWORD", "").lower() in ("1", "true", "yes")
tables = None
error = None
try:
with connect() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"""
SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schemaname, tablename
"""
)
tables = cur.fetchall()
except ConnectionError as exc:
error = str(exc)
except psycopg2.Error as exc:
error = f"Ошибка базы данных: {exc}"
if show_password:
cfg["password"] = "***"
return render_template(
"index.html",
cfg=cfg,
tables=tables,
error=error,
env=os.getenv("APP_ENV", "production"),
)
@app.route("/table")
def table():
"""Просмотр содержимого таблицы с пагинацией."""
schema = request.args.get("schema", "public")
name = request.args.get("name", "")
page = max(request.args.get("page", 1, type=int), 1)
per_page = min(max(request.args.get("per_page", 50, type=int), 1), 500)
if not name:
return redirect(url_for("index"))
full_name = f'"{schema}"."{name}"'
rows = None
columns = None
count = None
error = None
try:
with connect() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
# Проверяем, что таблица существует
cur.execute(
"""
SELECT to_regclass(%s) AS reg
""",
(full_name,),
)
if cur.fetchone()["reg"] is None:
abort(404, f"Таблица {full_name} не найдена")
cur.execute(f'SELECT COUNT(*) AS n FROM {full_name}')
count = cur.fetchone()["n"]
offset = (page - 1) * per_page
cur.execute(
f'SELECT * FROM {full_name} ORDER BY 1 LIMIT %s OFFSET %s',
(per_page, offset),
)
rows = cur.fetchall()
if rows:
columns = list(rows[0].keys())
except ConnectionError as exc:
error = str(exc)
except psycopg2.Error as exc:
error = f"Ошибка базы данных: {exc}"
return render_template(
"table.html",
schema=schema,
name=name,
columns=columns,
rows=rows,
count=count,
page=page,
per_page=per_page,
error=error,
)
@app.route("/query", methods=["GET", "POST"])
def query():
"""Простейшая консоль SQL (read-only SELECT)."""
sql = request.form.get("sql", "") if request.method == "POST" else request.args.get("sql", "")
columns = None
rows = None
error = None
elapsed = None
if sql:
sql_clean = sql.strip().rstrip(";")
if not sql_clean.lower().lstrip().startswith("select"):
error = "Разрешены только SELECT-запросы (read-only режим)"
else:
import time
start = time.time()
try:
with connect() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(sql_clean)
if cur.description:
max_rows = int(os.getenv("QUERY_MAX_ROWS", "200"))
rows = cur.fetchmany(max_rows)
columns = list(rows[0].keys()) if rows else [d.name for d in cur.description]
except ConnectionError as exc:
error = str(exc)
except psycopg2.Error as exc:
error = f"Ошибка базы данных: {exc}"
elapsed = round(time.time() - start, 3)
return render_template(
"query.html",
sql=sql,
columns=columns,
rows=rows,
error=error,
elapsed=elapsed,
QUERY_MAX_ROWS=int(os.getenv("QUERY_MAX_ROWS", "200")),
)
@app.errorhandler(404)
def not_found(exc):
return render_template("error.html", code=404, message=exc.description or "Страница не найдена"), 404
if __name__ == "__main__":
app.run(debug=os.getenv("FLASK_DEBUG", "true").lower() in ("1", "true", "yes"),
host="0.0.0.0",
port=int(os.getenv("PORT", "5000")))