기능 추가

This commit is contained in:
2026-01-05 13:15:49 +09:00
parent 9c7ba65c32
commit dcdcfecc40
12 changed files with 525 additions and 81 deletions

172
app.py
View File

@@ -1,12 +1,14 @@
from flask import Flask, render_template, request, send_file, abort
from flask import (
Flask, render_template, request,
send_file, abort, redirect, url_for
)
import subprocess
import uuid
import shutil
from pathlib import Path
from datetime import datetime
import zipfile
import yaml # ⭐ 추가
#app = Flask(__name__)
app = Flask(__name__, static_folder="static")
BASE = Path("/work")
@@ -17,12 +19,61 @@ def ts():
return datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
# ===== backup YAML sanitize =====
REMOVE_METADATA_FIELDS = {
"uid",
"resourceVersion",
"generation",
"creationTimestamp",
"managedFields",
}
def sanitize_backup_yaml(path: Path):
docs = list(yaml.safe_load_all(path.read_text()))
cleaned = []
for doc in docs:
if not isinstance(doc, dict):
cleaned.append(doc)
continue
# metadata 정리
meta = doc.get("metadata")
if isinstance(meta, dict):
for k in list(meta.keys()):
if k in REMOVE_METADATA_FIELDS:
meta.pop(k, None)
# status 제거 (rollback 충돌 방지)
doc.pop("status", None)
cleaned.append(doc)
path.write_text(
yaml.safe_dump_all(
cleaned,
sort_keys=False
)
)
def sanitize_backup_tree(backup_dir: Path):
if not backup_dir.exists():
return
for p in backup_dir.rglob("*.yaml"):
sanitize_backup_yaml(p)
# ===== routes =====
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
job_id = str(uuid.uuid4())
job_dir = BASE / job_id
job_dir.mkdir(parents=True)
job_dir.mkdir(parents=True, exist_ok=True)
cmd = [
"python3",
@@ -33,33 +84,41 @@ def index():
if request.form.get("from_class"):
cmd += ["--from-ingress-class", request.form["from_class"]]
if request.form.get("to_class"):
cmd += ["--ingress-class", request.form["to_class"]]
if request.form.get("namespace"):
cmd += ["--namespace", request.form["namespace"]]
subprocess.check_call(cmd, cwd=job_dir)
timestamp = ts()
# 🔥 backup YAML 정화
sanitize_backup_tree(job_dir / "backup")
# result.zip (yaml + report)
timestamp = ts()
result_zip = BASE / f"{timestamp}-result.zip"
backup_zip = BASE / f"{timestamp}-backup.zip"
# result zip
with zipfile.ZipFile(result_zip, "w", zipfile.ZIP_DEFLATED) as z:
for p in (job_dir / "yaml").rglob("*"):
z.write(p, p.relative_to(job_dir))
if p.is_file():
z.write(p, p.relative_to(job_dir))
report = job_dir / "migration-report.md"
if report.exists():
z.write(report, report.relative_to(job_dir))
# backup.zip
backup_zip = BASE / f"{timestamp}-backup.zip"
# backup zip
with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as z:
for p in (job_dir / "backup").rglob("*"):
z.write(p, p.relative_to(job_dir))
if p.is_file():
z.write(p, p.relative_to(job_dir))
return preview(job_id, result_zip.name, backup_zip.name)
return redirect(url_for(
"preview",
job_id=job_id,
result_zip=result_zip.name,
backup_zip=backup_zip.name
))
return render_template("index.html")
@@ -68,13 +127,14 @@ def index():
def preview(job_id, result_zip, backup_zip):
job_dir = BASE / job_id
def tree(root: Path):
def tree(root):
t = {}
if not root.exists():
return t
for ns in sorted(root.iterdir()):
if ns.is_dir():
t[ns.name] = sorted(f.name for f in ns.iterdir() if f.is_file())
if root.exists():
for ns in root.iterdir():
if ns.is_dir():
t[ns.name] = sorted(
f.name for f in ns.iterdir() if f.is_file()
)
return t
return render_template(
@@ -87,14 +147,35 @@ def preview(job_id, result_zip, backup_zip):
)
@app.route("/view/<job_id>/<kind>/<path:path>")
@app.route("/view/<job_id>/<kind>/<path:path>", methods=["GET", "POST"])
def view_file(job_id, kind, path):
if kind not in ("yaml", "backup"):
abort(404)
f = BASE / job_id / kind / path
if not f.exists():
if not f.exists() or not f.is_file():
abort(404)
return f"<pre>{f.read_text()}</pre>"
editable = (kind == "yaml")
if request.method == "POST":
if not editable:
abort(403)
f.write_text(request.form.get("content", ""))
return redirect(url_for(
"view_file",
job_id=job_id,
kind=kind,
path=path
))
return render_template(
"view.html",
title=f"{kind.upper()} / {path}",
content=f.read_text(),
editable=editable
)
@app.route("/report/<job_id>")
@@ -102,7 +183,52 @@ def view_report(job_id):
report = BASE / job_id / "migration-report.md"
if not report.exists():
abort(404)
return f"<pre>{report.read_text()}</pre>"
return render_template(
"view.html",
title="Migration Report",
content=report.read_text()
)
@app.route("/apply/<job_id>/<kind>", methods=["POST"])
def apply_yaml(job_id, kind):
if kind not in ("yaml", "backup"):
abort(400)
target_dir = BASE / job_id / kind
if not target_dir.exists():
abort(404)
if kind == "yaml":
cmd = ["kubectl", "replace", "-f", str(target_dir), "--recursive"]
title = "Apply Converted YAML"
else:
cmd = ["kubectl", "replace", "-f", str(target_dir), "--recursive"]
title = "Rollback from Backup"
proc = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
output = ""
if proc.stdout:
output += proc.stdout
if proc.stderr:
output += "\n" + proc.stderr
status = "SUCCESS" if proc.returncode == 0 else "FAILED"
return render_template(
"apply_result.html",
title=title,
status=status,
command=" ".join(cmd),
output=output
)
@app.route("/download/<name>")