Files
appfactory-portal/app/db/migration_readiness.py
T
JiriUhlir f101d91821 Upraveno. Portál už nečte ani neinterpretuje READY/NOT READY z textu výstupu a nepřidává žádnou logiku pro disk, backup, Gitea ani Registry.
Změny:
migration_readiness.py (line 140) teď určuje READY pouze jako returncode == 0; chybějící nebo nenulový return code je NOT READY.
Preflight UI v routes/migration_readiness.py (line 85) zobrazuje Return code, summary OK/WARN/FAIL a oddělené raw stdout / stderr.
Odstraněná vlastní logika, která hledala READY/NOT READY ve výstupu skriptu.
2026-06-08 08:15:27 +02:00

230 lines
6.6 KiB
Python

import json
import re
from typing import Any
from app.db.database import get_connection
from app.db.migrations import run_migrations
PREFLIGHT_SCRIPT = "preflight-check.sh"
BOOTSTRAP_SCRIPT = "bootstrap-v2.sh"
SENSITIVE_KEY_RE = re.compile(
r"(SECRET|TOKEN|PASSWORD|PASS|KEY|CREDENTIAL|AUTH|API_KEY|PRIVATE)",
re.IGNORECASE,
)
def _job_matches_script(row: dict[str, Any], script_name: str) -> bool:
if row.get("target_id") == script_name:
return True
try:
payload = json.loads(row.get("payload_json") or "{}")
except (TypeError, ValueError):
return False
return payload.get("script_name") == script_name
def get_latest_script_job(script_name: str):
run_migrations()
con = get_connection()
rows = con.execute(
"""
SELECT *
FROM jobs
WHERE type = 'run_script'
AND (
target_id = ?
OR payload_json LIKE ?
)
ORDER BY id DESC
LIMIT 20
""",
(script_name, f'%"{script_name}"%'),
).fetchall()
con.close()
for row in rows:
job = dict(row)
if _job_matches_script(job, script_name):
return job
return None
def get_latest_preflight_job():
return get_latest_script_job(PREFLIGHT_SCRIPT)
def get_latest_bootstrap_job():
return get_latest_script_job(BOOTSTRAP_SCRIPT)
def get_job_log_entries(job_id: int) -> list[dict[str, Any]]:
run_migrations()
con = get_connection()
rows = con.execute(
"""
SELECT stream, message
FROM job_logs
WHERE job_id = ?
ORDER BY id
""",
(job_id,),
).fetchall()
con.close()
return [dict(row) for row in rows]
def _redact_sensitive_text(value: str) -> str:
redacted_lines = []
for line in value.splitlines():
env_match = re.match(r"^(\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=\s*)(.*)$", line)
if env_match and SENSITIVE_KEY_RE.search(env_match.group(2)):
redacted_lines.append(f"{env_match.group(1)}[redacted]")
continue
json_match = re.match(r'^(\s*["\']?([^"\':=]+)["\']?\s*[:=]\s*)(.*)$', line)
if json_match and SENSITIVE_KEY_RE.search(json_match.group(2)):
redacted_lines.append(f"{json_match.group(1)}[redacted]")
continue
redacted_lines.append(line)
return "\n".join(redacted_lines)
def get_job_log_output(job_id: int, stream: str | None = None, include_stream: bool = True) -> str:
rows = get_job_log_entries(job_id)
lines = []
for row in rows:
row_stream = row.get("stream") or "log"
if stream and row_stream != stream:
continue
message = _redact_sensitive_text(row.get("message") or "")
lines.append(f"[{row_stream}] {message}" if include_stream else message)
return "\n".join(lines)
def _count_token(text: str, token: str) -> int:
return len(re.findall(rf"(?<![A-Z0-9_]){re.escape(token)}(?![A-Z0-9_])", text.upper()))
def _result_data(job: dict[str, Any]) -> dict[str, Any]:
try:
value = json.loads(job.get("result_json") or "{}")
except (TypeError, ValueError):
return {}
return value if isinstance(value, dict) else {}
def _result_value(job: dict[str, Any], keys: tuple[str, ...]) -> Any:
data = _result_data(job)
for key in keys:
if key in data:
return data.get(key)
return None
def _first_int(data: dict[str, Any], keys: tuple[str, ...]) -> int | None:
for key in keys:
value = data.get(key)
if value is None:
continue
try:
return int(value)
except (TypeError, ValueError):
continue
return None
def summarize_readiness(job: dict[str, Any] | None) -> dict[str, Any]:
if not job:
return {
"ready": False,
"ok_count": 0,
"warn_count": 0,
"fail_count": 0,
"return_code": None,
"stdout": "",
"stderr": "",
"job": None,
}
result_data = _result_data(job)
return_code = _result_value(job, ("returncode", "return_code", "exit_code", "exitCode"))
stdout = _result_value(job, ("stdout", "out"))
stderr = _result_value(job, ("stderr", "err"))
if stdout is None:
stdout = get_job_log_output(int(job["id"]), stream="stdout", include_stream=False)
if stderr is None:
stderr = get_job_log_output(int(job["id"]), stream="stderr", include_stream=False)
error_text = job.get("error_text") or ""
if error_text:
stderr = f"{stderr}\n{error_text}" if stderr else error_text
stdout = _redact_sensitive_text(str(stdout or ""))
stderr = _redact_sensitive_text(str(stderr or ""))
combined_text = "\n".join(
value
for value in (
stdout,
stderr,
job.get("result_json") or "",
)
if value
)
ok_count = _first_int(result_data, ("ok", "ok_count", "okCount"))
warn_count = _first_int(result_data, ("warn", "warn_count", "warnCount", "warnings"))
fail_count = _first_int(result_data, ("fail", "fail_count", "failCount", "failed", "errors"))
if ok_count is None:
ok_count = _count_token(combined_text, "OK")
if warn_count is None:
warn_count = _count_token(combined_text, "WARN")
if fail_count is None:
fail_count = _count_token(combined_text, "FAIL")
return {
"ready": return_code == 0,
"ok_count": ok_count,
"warn_count": warn_count,
"fail_count": fail_count,
"return_code": return_code,
"stdout": stdout,
"stderr": stderr,
"job": job,
}
def summarize_script_run(job: dict[str, Any] | None) -> dict[str, Any]:
if not job:
return {
"job": None,
"return_code": None,
"stdout": "",
"stderr": "",
}
return_code = _result_value(job, ("returncode", "return_code", "exit_code", "exitCode"))
stdout = _result_value(job, ("stdout", "out"))
stderr = _result_value(job, ("stderr", "err"))
if stdout is None:
stdout = get_job_log_output(int(job["id"]), stream="stdout", include_stream=False)
if stderr is None:
stderr = get_job_log_output(int(job["id"]), stream="stderr", include_stream=False)
error_text = job.get("error_text") or ""
if error_text:
stderr = f"{stderr}\n{error_text}" if stderr else error_text
return {
"job": job,
"return_code": return_code,
"stdout": _redact_sensitive_text(str(stdout or "")),
"stderr": _redact_sensitive_text(str(stderr or "")),
}