244 lines
5.9 KiB
Python
244 lines
5.9 KiB
Python
from flask import (
|
|
Flask, render_template, request,
|
|
send_file, abort, redirect, url_for
|
|
)
|
|
import subprocess
|
|
import uuid
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import zipfile
|
|
import yaml # ⭐ 추가
|
|
|
|
app = Flask(__name__, static_folder="static")
|
|
|
|
BASE = Path("/work")
|
|
BASE.mkdir(exist_ok=True)
|
|
|
|
|
|
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, exist_ok=True)
|
|
|
|
cmd = [
|
|
"python3",
|
|
"/app/migrator.py",
|
|
"--split",
|
|
"--mapping", "/app/mapping.yaml",
|
|
]
|
|
|
|
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)
|
|
|
|
# 🔥 backup YAML 정화
|
|
sanitize_backup_tree(job_dir / "backup")
|
|
|
|
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("*"):
|
|
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
|
|
with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as z:
|
|
for p in (job_dir / "backup").rglob("*"):
|
|
if p.is_file():
|
|
z.write(p, p.relative_to(job_dir))
|
|
|
|
return redirect(url_for(
|
|
"preview",
|
|
job_id=job_id,
|
|
result_zip=result_zip.name,
|
|
backup_zip=backup_zip.name
|
|
))
|
|
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route("/preview/<job_id>/<result_zip>/<backup_zip>")
|
|
def preview(job_id, result_zip, backup_zip):
|
|
job_dir = BASE / job_id
|
|
|
|
def tree(root):
|
|
t = {}
|
|
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(
|
|
"preview.html",
|
|
job_id=job_id,
|
|
yaml_tree=tree(job_dir / "yaml"),
|
|
backup_tree=tree(job_dir / "backup"),
|
|
result_zip=result_zip,
|
|
backup_zip=backup_zip,
|
|
)
|
|
|
|
|
|
@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() or not f.is_file():
|
|
abort(404)
|
|
|
|
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>")
|
|
def view_report(job_id):
|
|
report = BASE / job_id / "migration-report.md"
|
|
if not report.exists():
|
|
abort(404)
|
|
|
|
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>")
|
|
def download(name):
|
|
path = BASE / name
|
|
if not path.exists():
|
|
abort(404)
|
|
return send_file(path, as_attachment=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=8080, debug=False)
|