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///") 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///", 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/") 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//", 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 = "Replace 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/") 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)