웹 UI 추가 및 Dockerfile 추가
This commit is contained in:
40
Dockerfile
40
Dockerfile
@@ -1,36 +1,20 @@
|
|||||||
# ===============================
|
|
||||||
# Stage 1: kubectl
|
|
||||||
# ===============================
|
|
||||||
FROM bitnami/kubectl AS kubectl
|
|
||||||
|
|
||||||
|
|
||||||
# ===============================
|
|
||||||
# Stage 2: runtime
|
|
||||||
# ===============================
|
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# kubectl 복사
|
RUN apt-get update && \
|
||||||
COPY --from=kubectl /opt/bitnami/kubectl/bin/kubectl /usr/local/bin/kubectl
|
apt-get install -y curl && \
|
||||||
|
curl -LO https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl && \
|
||||||
|
install -m 0755 kubectl /usr/local/bin/kubectl && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 필수 패키지
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# python 패키지
|
|
||||||
RUN pip install --no-cache-dir ruamel.yaml
|
|
||||||
|
|
||||||
# 작업 디렉토리
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 스크립트 복사
|
COPY migrator.py mapping.yaml app.py requirements.txt ./
|
||||||
COPY migrate.py /app/migrate.py
|
COPY templates ./templates
|
||||||
|
|
||||||
# 기본 mapping.yaml
|
RUN pip install --no-cache-dir -r requirements.txt gunicorn
|
||||||
COPY mapping.yaml /app/mapping.yaml
|
|
||||||
|
|
||||||
# 실행 권한
|
RUN mkdir /work
|
||||||
RUN chmod +x /app/migrate.py
|
|
||||||
|
|
||||||
# 기본 엔트리포인트
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["python", "/app/migrate.py"]
|
|
||||||
|
CMD ["gunicorn", "-b", "0.0.0.0:8080", "--timeout", "300", "app:app"]
|
||||||
|
|||||||
117
app.py
Normal file
117
app.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
from flask import Flask, render_template, request, send_file, abort
|
||||||
|
import subprocess
|
||||||
|
import uuid
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
#app = Flask(__name__)
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
timestamp = ts()
|
||||||
|
|
||||||
|
# result.zip (yaml + report)
|
||||||
|
result_zip = BASE / f"{timestamp}-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))
|
||||||
|
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"
|
||||||
|
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))
|
||||||
|
|
||||||
|
return preview(job_id, result_zip.name, 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: Path):
|
||||||
|
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())
|
||||||
|
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>")
|
||||||
|
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():
|
||||||
|
abort(404)
|
||||||
|
return f"<pre>{f.read_text()}</pre>"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/report/<job_id>")
|
||||||
|
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>"
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
313
migrator.py
Normal file
313
migrator.py
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# YAML 설정
|
||||||
|
# -----------------------------
|
||||||
|
yaml = YAML()
|
||||||
|
yaml.preserve_quotes = True
|
||||||
|
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||||
|
yaml.width = 4096
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# kubectl ingress 조회
|
||||||
|
# -----------------------------
|
||||||
|
def kubectl_ingress(namespace=None):
|
||||||
|
cmd = ["kubectl", "get", "ingress"]
|
||||||
|
|
||||||
|
if namespace:
|
||||||
|
cmd += ["-n", namespace]
|
||||||
|
else:
|
||||||
|
cmd.append("--all-namespaces")
|
||||||
|
|
||||||
|
cmd += ["-o", "yaml"]
|
||||||
|
|
||||||
|
out = subprocess.check_output(cmd)
|
||||||
|
return yaml.load(out)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# mapping.yaml 로드
|
||||||
|
# -----------------------------
|
||||||
|
def load_mapping(path):
|
||||||
|
with open(path) as f:
|
||||||
|
return yaml.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# annotation 렌더링
|
||||||
|
# -----------------------------
|
||||||
|
def render_annotations_block(unsupported, partial, converted):
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
if unsupported:
|
||||||
|
note = unsupported[0]["note"]
|
||||||
|
lines.append(f" # {note}")
|
||||||
|
for u in unsupported:
|
||||||
|
lines.append(f" #{u['key']}: {u['value']}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if partial:
|
||||||
|
for p in partial:
|
||||||
|
lines.append(f" # PARTIAL SUPPORT: {p['note']}")
|
||||||
|
lines.append(f" {p['haproxy']}: \"{str(p['value'])}\"")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for c in converted:
|
||||||
|
lines.append(f" {c['haproxy']}: \"{str(c['value'])}\"")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# metadata 정리
|
||||||
|
# -----------------------------
|
||||||
|
def cleanup_metadata(meta):
|
||||||
|
for k in [
|
||||||
|
"creationTimestamp",
|
||||||
|
"generation",
|
||||||
|
"resourceVersion",
|
||||||
|
"uid",
|
||||||
|
"managedFields",
|
||||||
|
]:
|
||||||
|
meta.pop(k, None)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# ingressClassName 기준 필터링
|
||||||
|
# -----------------------------
|
||||||
|
def filter_by_ingress_class(data, from_ingress_class):
|
||||||
|
filtered = deepcopy(data)
|
||||||
|
filtered["items"] = []
|
||||||
|
|
||||||
|
for item in data.get("items", []):
|
||||||
|
spec = item.get("spec", {})
|
||||||
|
current_class = spec.get("ingressClassName", "nginx")
|
||||||
|
|
||||||
|
if current_class == from_ingress_class:
|
||||||
|
filtered["items"].append(item)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 변환 전 원본 백업 (backup/ns/ingress.yaml)
|
||||||
|
# -----------------------------
|
||||||
|
def backup_original_ingress(data, backup_dir="./backup"):
|
||||||
|
base = Path(backup_dir)
|
||||||
|
base.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for item in data.get("items", []):
|
||||||
|
meta = item.get("metadata", {})
|
||||||
|
name = meta.get("name")
|
||||||
|
namespace = meta.get("namespace", "default")
|
||||||
|
|
||||||
|
ns_dir = base / namespace
|
||||||
|
ns_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with (ns_dir / f"{name}.yaml").open("w") as f:
|
||||||
|
yaml.dump(item, f)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# ingress 변환 로직
|
||||||
|
# -----------------------------
|
||||||
|
def migrate_ingress(data, mapping, to_ingress_class=None):
|
||||||
|
report = {
|
||||||
|
"converted": [],
|
||||||
|
"partial": [],
|
||||||
|
"unsupported": [],
|
||||||
|
"detail": defaultdict(lambda: {"partial": [], "unsupported": []}),
|
||||||
|
}
|
||||||
|
|
||||||
|
new = deepcopy(data)
|
||||||
|
|
||||||
|
for item in new.get("items", []):
|
||||||
|
meta = item.setdefault("metadata", {})
|
||||||
|
spec = item.setdefault("spec", {})
|
||||||
|
|
||||||
|
cleanup_metadata(meta)
|
||||||
|
|
||||||
|
name = meta.get("name")
|
||||||
|
namespace = meta.get("namespace", "default")
|
||||||
|
ingress_id = f"{namespace}/{name}"
|
||||||
|
|
||||||
|
if to_ingress_class:
|
||||||
|
spec["ingressClassName"] = to_ingress_class
|
||||||
|
|
||||||
|
anns = meta.get("annotations", {}) or {}
|
||||||
|
|
||||||
|
unsupported = []
|
||||||
|
partial = []
|
||||||
|
converted = []
|
||||||
|
|
||||||
|
for k, v in anns.items():
|
||||||
|
rule = mapping.get(k)
|
||||||
|
|
||||||
|
if not rule:
|
||||||
|
converted.append({"haproxy": k, "value": v})
|
||||||
|
continue
|
||||||
|
|
||||||
|
support = rule.get("support", "unsupported")
|
||||||
|
haproxy_key = rule.get("haproxy")
|
||||||
|
note = rule.get("note", "no HAProxy equivalent")
|
||||||
|
|
||||||
|
if support == "full" and haproxy_key:
|
||||||
|
converted.append({"haproxy": haproxy_key, "value": v})
|
||||||
|
report["converted"].append(k)
|
||||||
|
|
||||||
|
elif support == "partial" and haproxy_key:
|
||||||
|
partial.append({"haproxy": haproxy_key, "value": v, "note": note})
|
||||||
|
report["partial"].append(k)
|
||||||
|
report["detail"][ingress_id]["partial"].append(
|
||||||
|
{"nginx": k, "haproxy": haproxy_key, "note": note}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
unsupported.append({"key": k, "value": v, "note": note})
|
||||||
|
report["unsupported"].append(k)
|
||||||
|
report["detail"][ingress_id]["unsupported"].append(
|
||||||
|
{"nginx": k, "value": v, "note": note}
|
||||||
|
)
|
||||||
|
|
||||||
|
meta.pop("annotations", None)
|
||||||
|
item["_rendered_annotations"] = render_annotations_block(
|
||||||
|
unsupported, partial, converted
|
||||||
|
)
|
||||||
|
|
||||||
|
return new, report
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 리포트 생성
|
||||||
|
# -----------------------------
|
||||||
|
def write_report(report, path):
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write("# Ingress Migration Report\n\n")
|
||||||
|
f.write("## Summary\n")
|
||||||
|
f.write(f"- Converted : {len(report['converted'])}\n")
|
||||||
|
f.write(f"- Partial : {len(report['partial'])}\n")
|
||||||
|
f.write(f"- Unsupported: {len(report['unsupported'])}\n\n")
|
||||||
|
f.write("---\n\n")
|
||||||
|
|
||||||
|
for ingress, detail in report["detail"].items():
|
||||||
|
if not detail["partial"] and not detail["unsupported"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
f.write(f"## {ingress}\n\n")
|
||||||
|
|
||||||
|
for p in detail["partial"]:
|
||||||
|
f.write(
|
||||||
|
f"- PARTIAL {p['nginx']} → {p['haproxy']}\n"
|
||||||
|
f" - note: {p['note']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
for u in detail["unsupported"]:
|
||||||
|
f.write(
|
||||||
|
f"- UNSUPPORTED {u['nginx']}\n"
|
||||||
|
f" - note: {u['note']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 파일 저장 (yaml/ns/ingress.yaml)
|
||||||
|
# -----------------------------
|
||||||
|
def save_split(data, out_dir="./yaml"):
|
||||||
|
base = Path(out_dir)
|
||||||
|
base.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for item in data.get("items", []):
|
||||||
|
rendered = item.pop("_rendered_annotations", "")
|
||||||
|
meta = item["metadata"]
|
||||||
|
name = meta["name"]
|
||||||
|
namespace = meta.get("namespace", "default")
|
||||||
|
|
||||||
|
ns_dir = base / namespace
|
||||||
|
ns_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
path = ns_dir / f"{name}.yaml"
|
||||||
|
|
||||||
|
with path.open("w") as f:
|
||||||
|
yaml.dump(item, f)
|
||||||
|
|
||||||
|
if rendered:
|
||||||
|
content = path.read_text()
|
||||||
|
content = content.replace(
|
||||||
|
"metadata:\n",
|
||||||
|
"metadata:\n annotations:\n" + rendered + "\n",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
path.write_text(content)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# main
|
||||||
|
# -----------------------------
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="nginx ingress → HAProxy ingress migration utility"
|
||||||
|
)
|
||||||
|
parser.add_argument("--mapping", default="mapping.yaml")
|
||||||
|
parser.add_argument("--split", action="store_true")
|
||||||
|
parser.add_argument("--out", default="./yaml")
|
||||||
|
parser.add_argument("--ingress-class", help="target ingressClassName")
|
||||||
|
parser.add_argument(
|
||||||
|
"--from-ingress-class",
|
||||||
|
default="nginx",
|
||||||
|
help="source ingressClassName (default: nginx)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--namespace",
|
||||||
|
help="target namespace (default: all namespaces)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--report", default="migration-report.md")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.split:
|
||||||
|
parser.error("--split must be specified")
|
||||||
|
|
||||||
|
# 디렉토리 보장
|
||||||
|
Path("./backup").mkdir(parents=True, exist_ok=True)
|
||||||
|
Path(args.out).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
ingress = kubectl_ingress(args.namespace)
|
||||||
|
|
||||||
|
ingress = filter_by_ingress_class(
|
||||||
|
ingress,
|
||||||
|
args.from_ingress_class,
|
||||||
|
)
|
||||||
|
|
||||||
|
backup_original_ingress(ingress)
|
||||||
|
|
||||||
|
mapping = load_mapping(args.mapping)
|
||||||
|
|
||||||
|
converted, report = migrate_ingress(
|
||||||
|
ingress,
|
||||||
|
mapping,
|
||||||
|
to_ingress_class=args.ingress_class,
|
||||||
|
)
|
||||||
|
|
||||||
|
save_split(converted, args.out)
|
||||||
|
write_report(report, args.report)
|
||||||
|
|
||||||
|
print("\n=== Migration Summary ===")
|
||||||
|
print(f"Converted : {len(report['converted'])}")
|
||||||
|
print(f"Partial : {len(report['partial'])}")
|
||||||
|
print(f"Unsupported: {len(report['unsupported'])}")
|
||||||
|
print(f"Backup : ./backup/<namespace>/<ingress>.yaml")
|
||||||
|
print(f"Output : {args.out}/<namespace>/<ingress>.yaml")
|
||||||
|
print(f"Report : {args.report}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
flask
|
||||||
|
ruamel.yaml
|
||||||
|
gunicorn
|
||||||
73
static/style.css
Normal file
73
static/style.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #020617;
|
||||||
|
border: 1px solid #1e293b;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 6px 0 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: #020617;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #2563eb;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #60a5fa;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #020617;
|
||||||
|
border: 1px solid #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
27
templates/index.html
Normal file
27
templates/index.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Ingress Migrator</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Ingress Migrator</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<form method="post">
|
||||||
|
<label>From ingressClass</label>
|
||||||
|
<input name="from_class" value="nginx">
|
||||||
|
|
||||||
|
<label>To ingressClass</label>
|
||||||
|
<input name="to_class" value="haproxy">
|
||||||
|
|
||||||
|
<label>Namespace (optional)</label>
|
||||||
|
<input name="namespace">
|
||||||
|
|
||||||
|
<button type="submit">Run Migration</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
templates/preview.html
Normal file
44
templates/preview.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Result</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1>Migration Result</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Converted YAML</h2>
|
||||||
|
{% for ns, files in yaml_tree.items() %}
|
||||||
|
<h4>{{ ns }}</h4>
|
||||||
|
<ul>
|
||||||
|
{% for f in files %}
|
||||||
|
<li><a href="/view/{{ job_id }}/yaml/{{ ns }}/{{ f }}">{{ f }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Original Backup</h2>
|
||||||
|
{% for ns, files in backup_tree.items() %}
|
||||||
|
<h4>{{ ns }}</h4>
|
||||||
|
<ul>
|
||||||
|
{% for f in files %}
|
||||||
|
<li><a href="/view/{{ job_id }}/backup/{{ ns }}/{{ f }}">{{ f }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<a href="/report/{{ job_id }}">📄 migration-report.md</a><br><br>
|
||||||
|
<a href="/download/{{ result_zip }}">⬇ result.zip</a><br>
|
||||||
|
<a href="/download/{{ backup_zip }}">⬇ backup.zip</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
templates/report.html
Normal file
2
templates/report.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<h2>Migration Report</h2>
|
||||||
|
<pre>{{ content }}</pre>
|
||||||
Reference in New Issue
Block a user