기능 추가
This commit is contained in:
172
app.py
172
app.py
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user