#!/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 from ruamel.yaml.comments import CommentedMap # ----------------------------- # YAML 설정 # ----------------------------- yaml = YAML() yaml.preserve_quotes = True yaml.indent(mapping=2, sequence=4, offset=2) # ----------------------------- # kubectl ingress 조회 # ----------------------------- def kubectl_ingress(): out = subprocess.check_output( ["kubectl", "get", "ingress", "--all-namespaces", "-o", "yaml"] ) return yaml.load(out) # ----------------------------- # mapping.yaml 로드 # ----------------------------- def load_mapping(path): with open(path) as f: return yaml.load(f) # ----------------------------- # ingress 변환 로직 # ----------------------------- def migrate_ingress(data, mapping, 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", {}) name = meta.get("name") namespace = meta.get("namespace", "default") ingress_id = f"{namespace}/{name}" if ingress_class: spec["ingressClassName"] = ingress_class anns = meta.get("annotations", {}) or {} new_anns = CommentedMap() for k, v in anns.items(): if k in mapping: rule = mapping[k] support = rule.get("support", "unsupported") haproxy_key = rule.get("haproxy") if support == "full": new_anns[haproxy_key] = v report["converted"].append(k) elif support == "partial": new_anns[haproxy_key] = v note = rule.get("note", "") new_anns.yaml_set_comment_before_after_key( haproxy_key, before=f"PARTIAL SUPPORT: {note}", ) report["partial"].append(k) report["detail"][ingress_id]["partial"].append( { "nginx": k, "haproxy": haproxy_key, "note": note, } ) else: lines = [f"UNSUPPORTED {k}:"] for line in str(v).splitlines(): lines.append(f" {line}") new_anns.yaml_set_end_comment("\n".join(lines)) report["unsupported"].append(k) report["detail"][ingress_id]["unsupported"].append( { "nginx": k, "value": v, } ) else: new_anns[k] = v meta["annotations"] = new_anns return new, report # ----------------------------- # 리포트 생성 (Markdown) # ----------------------------- 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") if any(v["partial"] for v in report["detail"].values()): f.write("## ⚠️ Partial Support\n\n") for ingress, items in report["detail"].items(): if not items["partial"]: continue f.write(f"### {ingress}\n") for p in items["partial"]: f.write( f"- {p['nginx']}\n" f" - mapped to: {p['haproxy']}\n" f" - note: {p['note']}\n" ) f.write("\n") if any(v["unsupported"] for v in report["detail"].values()): f.write("## ❌ Unsupported\n\n") for ingress, items in report["detail"].items(): if not items["unsupported"]: continue f.write(f"### {ingress}\n") for u in items["unsupported"]: f.write(f"- {u['nginx']}\n") f.write(" ```nginx\n") f.write(f"{u['value']}\n") f.write(" ```\n") f.write("\n") # ----------------------------- # 파일 저장 # ----------------------------- def save_single(data, out_path): with open(out_path, "w") as f: yaml.dump(data, f) def save_split(data, out_dir): Path(out_dir).mkdir(parents=True, exist_ok=True) for item in data.get("items", []): name = item["metadata"]["name"] namespace = item["metadata"].get("namespace", "default") path = Path(out_dir) / f"{namespace}__{name}.yaml" with open(path, "w") as f: yaml.dump(item, f) # ----------------------------- # main # ----------------------------- def main(): parser = argparse.ArgumentParser( description="nginx ingress → HAProxy ingress migration utility" ) parser.add_argument("--mapping", default="mapping.yaml") parser.add_argument("--single", action="store_true") parser.add_argument("--split", action="store_true") parser.add_argument("--out", default="output") parser.add_argument("--ingress-class") parser.add_argument( "--report", default="migration-report.md", help="migration summary report file", ) args = parser.parse_args() if not args.single and not args.split: parser.error("one of --single or --split must be specified") ingress = kubectl_ingress() mapping = load_mapping(args.mapping) converted, report = migrate_ingress( ingress, mapping, ingress_class=args.ingress_class, ) if args.single: out_file = args.out if args.out.endswith(".yaml") else f"{args.out}.yaml" save_single(converted, out_file) if args.split: 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"Report : {args.report}") if __name__ == "__main__": main()