기능 추가
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 subprocess
|
||||||
import uuid
|
import uuid
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import yaml # ⭐ 추가
|
||||||
|
|
||||||
#app = Flask(__name__)
|
|
||||||
app = Flask(__name__, static_folder="static")
|
app = Flask(__name__, static_folder="static")
|
||||||
|
|
||||||
BASE = Path("/work")
|
BASE = Path("/work")
|
||||||
@@ -17,12 +19,61 @@ def ts():
|
|||||||
return datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
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"])
|
@app.route("/", methods=["GET", "POST"])
|
||||||
def index():
|
def index():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
job_id = str(uuid.uuid4())
|
job_id = str(uuid.uuid4())
|
||||||
job_dir = BASE / job_id
|
job_dir = BASE / job_id
|
||||||
job_dir.mkdir(parents=True)
|
job_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"python3",
|
"python3",
|
||||||
@@ -33,33 +84,41 @@ def index():
|
|||||||
|
|
||||||
if request.form.get("from_class"):
|
if request.form.get("from_class"):
|
||||||
cmd += ["--from-ingress-class", request.form["from_class"]]
|
cmd += ["--from-ingress-class", request.form["from_class"]]
|
||||||
|
|
||||||
if request.form.get("to_class"):
|
if request.form.get("to_class"):
|
||||||
cmd += ["--ingress-class", request.form["to_class"]]
|
cmd += ["--ingress-class", request.form["to_class"]]
|
||||||
|
|
||||||
if request.form.get("namespace"):
|
if request.form.get("namespace"):
|
||||||
cmd += ["--namespace", request.form["namespace"]]
|
cmd += ["--namespace", request.form["namespace"]]
|
||||||
|
|
||||||
subprocess.check_call(cmd, cwd=job_dir)
|
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"
|
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:
|
with zipfile.ZipFile(result_zip, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
for p in (job_dir / "yaml").rglob("*"):
|
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"
|
report = job_dir / "migration-report.md"
|
||||||
if report.exists():
|
if report.exists():
|
||||||
z.write(report, report.relative_to(job_dir))
|
z.write(report, report.relative_to(job_dir))
|
||||||
|
|
||||||
# backup.zip
|
# backup zip
|
||||||
backup_zip = BASE / f"{timestamp}-backup.zip"
|
|
||||||
with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as z:
|
with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
for p in (job_dir / "backup").rglob("*"):
|
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")
|
return render_template("index.html")
|
||||||
|
|
||||||
@@ -68,13 +127,14 @@ def index():
|
|||||||
def preview(job_id, result_zip, backup_zip):
|
def preview(job_id, result_zip, backup_zip):
|
||||||
job_dir = BASE / job_id
|
job_dir = BASE / job_id
|
||||||
|
|
||||||
def tree(root: Path):
|
def tree(root):
|
||||||
t = {}
|
t = {}
|
||||||
if not root.exists():
|
if root.exists():
|
||||||
return t
|
for ns in root.iterdir():
|
||||||
for ns in sorted(root.iterdir()):
|
if ns.is_dir():
|
||||||
if ns.is_dir():
|
t[ns.name] = sorted(
|
||||||
t[ns.name] = sorted(f.name for f in ns.iterdir() if f.is_file())
|
f.name for f in ns.iterdir() if f.is_file()
|
||||||
|
)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
return render_template(
|
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):
|
def view_file(job_id, kind, path):
|
||||||
if kind not in ("yaml", "backup"):
|
if kind not in ("yaml", "backup"):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
f = BASE / job_id / kind / path
|
f = BASE / job_id / kind / path
|
||||||
if not f.exists():
|
if not f.exists() or not f.is_file():
|
||||||
abort(404)
|
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>")
|
@app.route("/report/<job_id>")
|
||||||
@@ -102,7 +183,52 @@ def view_report(job_id):
|
|||||||
report = BASE / job_id / "migration-report.md"
|
report = BASE / job_id / "migration-report.md"
|
||||||
if not report.exists():
|
if not report.exists():
|
||||||
abort(404)
|
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>")
|
@app.route("/download/<name>")
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
flask
|
flask
|
||||||
ruamel.yaml
|
ruamel.yaml
|
||||||
gunicorn
|
gunicorn
|
||||||
|
PyYAML
|
||||||
|
|||||||
135
static/style.css
135
static/style.css
@@ -1,12 +1,24 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #0f172a;
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: #020617;
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(180deg, #0b1220, #020617);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1100px;
|
max-width: 1400px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
@@ -14,43 +26,85 @@ body {
|
|||||||
.card {
|
.card {
|
||||||
background: #020617;
|
background: #020617;
|
||||||
border: 1px solid #1e293b;
|
border: 1px solid #1e293b;
|
||||||
border-radius: 10px;
|
border-radius: 14px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
h1 {
|
||||||
margin-top: 0;
|
margin: 0 0 24px;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======= FORM LAYOUT (GRID / NO OVERLAP) ======= */
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr auto;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
height: 44px;
|
||||||
padding: 10px;
|
padding: 10px 12px;
|
||||||
margin: 6px 0 14px;
|
font-size: 15px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
background: #020617;
|
background: #020617;
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: #2563eb;
|
height: 44px;
|
||||||
color: white;
|
padding: 0 26px;
|
||||||
|
border-radius: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 18px;
|
font-size: 15px;
|
||||||
border-radius: 6px;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #1d4ed8;
|
background: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======= VIEW / PREVIEW ======= */
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: transparent; /* ❌ 배경 제거 */
|
||||||
|
border: none; /* ❌ 테두리 제거 */
|
||||||
|
padding: 0; /* ❌ 중복 패딩 제거 */
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: auto;
|
||||||
|
color: #e5e7eb;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #60a5fa;
|
color: #60a5fa;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -60,14 +114,49 @@ a:hover {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
.actions-row {
|
||||||
padding-left: 18px;
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
.btn {
|
||||||
background: #020617;
|
display: inline-flex;
|
||||||
border: 1px solid #1e293b;
|
align-items: center;
|
||||||
border-radius: 8px;
|
justify-content: center;
|
||||||
padding: 16px;
|
height: 44px;
|
||||||
overflow-x: auto;
|
padding: 0 26px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 600px;
|
||||||
|
background: transparent;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|||||||
28
templates/apply_result.html
Normal file
28
templates/apply_result.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p><strong>Status:</strong> {{ status }}</p>
|
||||||
|
<p><strong>Command:</strong></p>
|
||||||
|
<pre>{{ command }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>kubectl output</h3>
|
||||||
|
<pre>{{ output }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card actions-row">
|
||||||
|
<button onclick="history.back()">Back</button>
|
||||||
|
<a class="btn" href="/">Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,15 +9,21 @@
|
|||||||
<h1>Ingress Migrator</h1>
|
<h1>Ingress Migrator</h1>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<form method="post">
|
<form method="post" class="form-grid">
|
||||||
<label>From ingressClass</label>
|
<div class="field">
|
||||||
<input name="from_class" value="nginx">
|
<label>From ingressClass</label>
|
||||||
|
<input name="from_class" value="nginx">
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>To ingressClass</label>
|
<div class="field">
|
||||||
<input name="to_class" value="haproxy">
|
<label>To ingressClass</label>
|
||||||
|
<input name="to_class" value="haproxy">
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>Namespace (optional)</label>
|
<div class="field">
|
||||||
<input name="namespace">
|
<label>Namespace (optional)</label>
|
||||||
|
<input name="namespace">
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit">Run Migration</button>
|
<button type="submit">Run Migration</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,44 +1,61 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Result</title>
|
<title>Ingress Migrator - Preview</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<h1>Migration Preview</h1>
|
||||||
<h1>Migration Result</h1>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Converted YAML</h2>
|
<h3>Converted YAML</h3>
|
||||||
{% for ns, files in yaml_tree.items() %}
|
<ul>
|
||||||
<h4>{{ ns }}</h4>
|
{% for ns, files in yaml_tree.items() %}
|
||||||
<ul>
|
<li><strong>{{ ns }}</strong>
|
||||||
{% for f in files %}
|
<ul>
|
||||||
<li><a href="/view/{{ job_id }}/yaml/{{ ns }}/{{ f }}">{{ f }}</a></li>
|
{% for f in files %}
|
||||||
{% endfor %}
|
<li>
|
||||||
</ul>
|
<a href="/view/{{ job_id }}/yaml/{{ ns }}/{{ f }}">{{ f }}</a>
|
||||||
{% endfor %}
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Original Backup</h2>
|
<h3>Original Backup</h3>
|
||||||
{% for ns, files in backup_tree.items() %}
|
<ul>
|
||||||
<h4>{{ ns }}</h4>
|
{% for ns, files in backup_tree.items() %}
|
||||||
<ul>
|
<li><strong>{{ ns }}</strong>
|
||||||
{% for f in files %}
|
<ul>
|
||||||
<li><a href="/view/{{ job_id }}/backup/{{ ns }}/{{ f }}">{{ f }}</a></li>
|
{% for f in files %}
|
||||||
{% endfor %}
|
<li>
|
||||||
</ul>
|
<a href="/view/{{ job_id }}/backup/{{ ns }}/{{ f }}">{{ f }}</a>
|
||||||
{% endfor %}
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card actions-row">
|
||||||
<a href="/report/{{ job_id }}">📄 migration-report.md</a><br><br>
|
<a class="btn" href="/report/{{ job_id }}">View Migration Report</a>
|
||||||
<a href="/download/{{ result_zip }}">⬇ result.zip</a><br>
|
<a class="btn" href="/download/{{ result_zip }}">Download Result ZIP</a>
|
||||||
<a href="/download/{{ backup_zip }}">⬇ backup.zip</a>
|
<a class="btn" href="/download/{{ backup_zip }}">Download Backup ZIP</a>
|
||||||
|
</div>
|
||||||
|
<div class="card actions-row">
|
||||||
|
<form method="post" action="/apply/{{ job_id }}/yaml">
|
||||||
|
<button class="btn btn-danger" type="submit">Replace Converted YAML</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/apply/{{ job_id }}/backup">
|
||||||
|
<button class="btn" type="submit">Rollback from Backup</button>
|
||||||
|
</form>
|
||||||
|
<button onclick="history.back()">Back</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,2 +1,20 @@
|
|||||||
<h2>Migration Report</h2>
|
<!DOCTYPE html>
|
||||||
<pre>{{ content }}</pre>
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Migration Report</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Migration Report</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<pre>{{ content }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card actions-row">
|
||||||
|
<button class="btn" onclick="history.back()">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
33
templates/view.html
Normal file
33
templates/view.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
{% if editable %}
|
||||||
|
<form method="post">
|
||||||
|
<textarea name="content" class="editor">{{ content }}</textarea>
|
||||||
|
|
||||||
|
<div class="actions-row" style="margin-top:16px;">
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
<button type="button" onclick="history.back()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<pre>{{ content }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not editable %}
|
||||||
|
<div class="card actions-row">
|
||||||
|
<button onclick="history.back()">Back</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
yaml/account.yaml
Normal file
26
yaml/account.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: ingress-migrator
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: ingress-migrator
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["networking.k8s.io"]
|
||||||
|
resources: ["ingresses"]
|
||||||
|
verbs: ["get", "list", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: ingress-migrator
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: ingress-migrator
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: ingress-migrator
|
||||||
|
namespace: testapp
|
||||||
57
yaml/configmap.yaml
Normal file
57
yaml/configmap.yaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ingress-migrator-mapping
|
||||||
|
data:
|
||||||
|
mapping.yaml: |
|
||||||
|
nginx.ingress.kubernetes.io/affinity:
|
||||||
|
haproxy: haproxy.org/cookie-persistence
|
||||||
|
support: partial
|
||||||
|
note: "value 수정 필요 (ex. cookie -> route)"
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/backend-protocol:
|
||||||
|
haproxy: haproxy.org/server-ssl
|
||||||
|
support: partial
|
||||||
|
note: "value 수정 필요 (ex. HTTPS -> true)"
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/force-ssl-redirect:
|
||||||
|
haproxy: haproxy.org/ssl-redirect
|
||||||
|
support: full
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/session-cookie-hash:
|
||||||
|
haproxy: null
|
||||||
|
support: unsupported
|
||||||
|
note: "HAProxy는 해시 방식 지정 불필요 (자동 제공)"
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/session-cookie-name:
|
||||||
|
haproxy: haproxy.org/cookie-persistence
|
||||||
|
support: full
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect:
|
||||||
|
haproxy: haproxy.org/ssl-redirect
|
||||||
|
support: full
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/server-snippet:
|
||||||
|
haproxy: haproxy.org/backend-config-snippet
|
||||||
|
support: partial
|
||||||
|
note: "value 수정 필요"
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/app-root:
|
||||||
|
haproxy: haproxy.org/path-rewrite
|
||||||
|
support: partial
|
||||||
|
note: "Change Value"
|
||||||
|
|
||||||
|
nginx.ingress.kubernetes.io/configuration-snippet:
|
||||||
|
haproxy: null
|
||||||
|
support: unsupported
|
||||||
|
note: "HAProxy does not support arbitrary NGINX snippets"
|
||||||
|
|
||||||
|
proxy-buffering:
|
||||||
|
haproxy: null
|
||||||
|
support: unsupported
|
||||||
|
note: "HAProxy does not support arbitrary NGINX snippets"
|
||||||
|
|
||||||
|
gzip-level:
|
||||||
|
haproxy: null
|
||||||
|
support: unsupported
|
||||||
|
note: "HAProxy does not support arbitrary NGINX snippets"
|
||||||
32
yaml/deploy.yaml
Normal file
32
yaml/deploy.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ingress-migrator-web
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ingress-migrator-web
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ingress-migrator-web
|
||||||
|
spec:
|
||||||
|
serviceAccountName: ingress-migrator
|
||||||
|
containers:
|
||||||
|
- name: web
|
||||||
|
image: sa8001/ngtoha:v3
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
volumeMounts:
|
||||||
|
- name: work
|
||||||
|
mountPath: /work
|
||||||
|
- name: mapping
|
||||||
|
mountPath: /app/mapping.yaml
|
||||||
|
subPath: mapping.yaml
|
||||||
|
volumes:
|
||||||
|
- name: work
|
||||||
|
emptyDir: {}
|
||||||
|
- name: mapping
|
||||||
|
configMap:
|
||||||
|
name: ingress-migrator-mapping
|
||||||
11
yaml/svc.yaml
Normal file
11
yaml/svc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: ingress-migrator-web
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: ingress-migrator-web
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
type: NodePort
|
||||||
Reference in New Issue
Block a user