Hotovo. Přidal jsem podporu bootstrap-v2.sh do stránky Migration Readiness.
Co se změnilo: migration_readiness.py (line 9) nově zná BOOTSTRAP_SCRIPT = "bootstrap-v2.sh". Helper umí najít poslední job pro bootstrap a připravit status, return code, started_at, finished_at, raw stdout/stderr z DB. routes/migration_readiness.py (line 111) přidává tlačítko Run Bootstrap v2, které pouze vytvoří run_script job s script_name=bootstrap-v2.sh. Přidal jsem výrazné varování k bootstrapu a blok “Poslední výsledek Bootstrap v2”. Zobrazení logů redaktuje env-like citlivé hodnoty typu TOKEN, PASSWORD, SECRET, API_KEY, PRIVATE, CREDENTIAL.
This commit is contained in:
@@ -6,10 +6,15 @@ 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]) -> bool:
|
||||
if row.get("target_id") == PREFLIGHT_SCRIPT:
|
||||
def _job_matches_script(row: dict[str, Any], script_name: str) -> bool:
|
||||
if row.get("target_id") == script_name:
|
||||
return True
|
||||
|
||||
try:
|
||||
@@ -17,10 +22,10 @@ def _job_matches_script(row: dict[str, Any]) -> bool:
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
return payload.get("script_name") == PREFLIGHT_SCRIPT
|
||||
return payload.get("script_name") == script_name
|
||||
|
||||
|
||||
def get_latest_preflight_job():
|
||||
def get_latest_script_job(script_name: str):
|
||||
run_migrations()
|
||||
con = get_connection()
|
||||
|
||||
@@ -36,18 +41,26 @@ def get_latest_preflight_job():
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
""",
|
||||
(PREFLIGHT_SCRIPT, f'%"{PREFLIGHT_SCRIPT}"%'),
|
||||
(script_name, f'%"{script_name}"%'),
|
||||
).fetchall()
|
||||
|
||||
con.close()
|
||||
for row in rows:
|
||||
job = dict(row)
|
||||
if _job_matches_script(job):
|
||||
if _job_matches_script(job, script_name):
|
||||
return job
|
||||
return None
|
||||
|
||||
|
||||
def get_job_log_output(job_id: int) -> str:
|
||||
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()
|
||||
|
||||
@@ -62,7 +75,34 @@ def get_job_log_output(job_id: int) -> str:
|
||||
).fetchall()
|
||||
|
||||
con.close()
|
||||
return "\n".join(f"[{row['stream'] or 'log'}] {row['message'] or ''}" for row in rows)
|
||||
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:
|
||||
@@ -77,6 +117,14 @@ def _result_data(job: dict[str, Any]) -> dict[str, Any]:
|
||||
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)
|
||||
@@ -157,3 +205,33 @@ def summarize_readiness(job: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"raw_output": raw_output,
|
||||
"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 "")),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user