feat: run.sh restart/stop/status + real DB connector + GitLab CI

1. run.sh: start/stop/restart/status/logs/build/test
   - PID file management in .pids/
   - Colored output, health check wait, log files
   - Auto alembic migrate on start

2. DB Connector (replaces mock aiomysql):
   - MySQL: aiomysql (connect/query/dml/test)
   - PostgreSQL: asyncpg (connect/query/dml/test)
   - SQLite: sqlite3 via executor (connect/query/dml/test)
   - Unified interface: DBConnector.test_connection/execute_query/execute_dml
   - Connection test returns version + latency

3. GitLab CI/CD pipeline:
   - test: backend pytest + frontend build
   - build: Docker images (manual trigger)
   - deploy: SSH deploy (manual trigger)
   - Cache: pip + node_modules

Tests: 128 passing, frontend build: OK
This commit is contained in:
logan committed 2026-05-22 15:16:59 +08:00
1 parent 64e0c37972
commit 2cfd45c9d1
10 files changed
+712 -126

No files matched your search

+79 -66
View File
@@ -1,79 +1,92 @@
import asyncio
from typing import List, Dict, Any
from typing import List
import aiomysql
from app.models.instance import Instance
from app.schemas.instance import ConnectionTestResult
from app.services.db_connector import DBConnector
class InstanceService:
@staticmethod
async def test_connection(instance: Instance) -> ConnectionTestResult:
try:
if instance.db_type == "mysql":
import aiomysql
conn = await asyncio.wait_for(
aiomysql.connect(
host=instance.host,
port=instance.port,
user=instance.username,
password=instance.password,
database=instance.database or None,
),
timeout=10,
)
async with conn.cursor() as cur:
await cur.execute("SELECT VERSION()")
version = (await cur.fetchone())[0]
conn.close()
return ConnectionTestResult(success=True, message="连接成功", version=str(version))
return ConnectionTestResult(success=False, message=f"暂不支持 {instance.db_type} 连接测试")
except asyncio.TimeoutError:
return ConnectionTestResult(success=False, message="连接超时")
except Exception as e:
return ConnectionTestResult(success=False, message=f"连接失败: {str(e)}")
@staticmethod
async def test_connection(instance: Instance) -> dict:
"""Test database connection using DBConnector."""
result = await DBConnector.test_connection(instance)
# Update instance status based on result
if result.success:
instance.status = "connected"
instance.version = result.version
else:
instance.status = "error"
return result.to_dict()
@staticmethod
async def list_databases(instance: Instance) -> List[str]:
if instance.db_type != "mysql":
"""List databases for a MySQL instance."""
if instance.db_type == "mysql":
conn = await aiomysql.connect(
host=instance.host, port=instance.port,
user=instance.username, password=instance.password,
)
try:
async with conn.cursor() as cur:
await cur.execute("SHOW DATABASES")
rows = await cur.fetchall()
return [row[0] for row in rows if row[0] not in ("information_schema", "mysql", "performance_schema", "sys")]
finally:
conn.close()
elif instance.db_type in ("postgres", "postgresql"):
import asyncpg
dsn = f"postgresql://{instance.username}:{instance.password}@{instance.host}:{instance.port}/postgres"
conn = await asyncpg.connect(dsn)
try:
rows = await conn.fetch("SELECT datname FROM pg_database WHERE datistemplate = false")
return [r["datname"] for r in rows]
finally:
await conn.close()
elif instance.db_type == "sqlite":
return [instance.database or "main"]
else:
return []
import aiomysql
conn = await aiomysql.connect(
host=instance.host, port=instance.port,
user=instance.username, password=instance.password,
)
async with conn.cursor() as cur:
await cur.execute("SHOW DATABASES")
rows = await cur.fetchall()
conn.close()
return [row[0] for row in rows if row[0] not in ("information_schema", "mysql", "performance_schema", "sys")]
@staticmethod
async def list_tables(instance: Instance, database: str) -> List[Dict[str, Any]]:
if instance.db_type != "mysql":
async def list_tables(instance: Instance, database: str) -> List[dict]:
"""List tables in a database."""
if instance.db_type == "mysql":
conn = await aiomysql.connect(
host=instance.host, port=instance.port,
user=instance.username, password=instance.password,
db=database,
)
try:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute(
"SELECT TABLE_NAME as name, TABLE_ROWS as rows, TABLE_TYPE as type "
"FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s",
(database,)
)
return await cur.fetchall()
finally:
conn.close()
elif instance.db_type in ("postgres", "postgresql"):
import asyncpg
dsn = f"postgresql://{instance.username}:{instance.password}@{instance.host}:{instance.port}/{database}"
conn = await asyncpg.connect(dsn)
try:
rows = await conn.fetch(
"SELECT tablename as name FROM pg_tables WHERE schemaname = 'public'"
)
return [dict(r) for r in rows]
finally:
await conn.close()
elif instance.db_type == "sqlite":
import sqlite3
db_path = instance.host
conn = sqlite3.connect(db_path)
cur = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [{"name": row[0]} for row in cur.fetchall()]
conn.close()
return tables
else:
return []
import aiomysql
conn = await aiomysql.connect(
host=instance.host, port=instance.port,
user=instance.username, password=instance.password,
database=database,
)
async with conn.cursor() as cur:
await cur.execute("SHOW TABLES")
tables = [row[0] for row in await cur.fetchall()]
result = []
for table in tables:
await cur.execute(f"SHOW TABLE STATUS LIKE '{table}'")
status = await cur.fetchone()
result.append({
"name": table,
"engine": status[1],
"rows": status[4],
"size": status[6],
"comment": status[17],
})
conn.close()
return result