From a1ecf4c634ca29a7b66d71cb16dfc2a67256db24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B3=80=EC=A0=95=ED=9B=88?= Date: Fri, 2 Jan 2026 16:31:41 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + migrate.py | 164 ++++++++++++++++++++++++++++++++--------------------- 2 files changed, 101 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index ead12cf..111ee4c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ --ingress-class : ingressClassName 설정 --report : report 파일 저장 경로 --mapping : 맵핑 파일 지정 (default : mapping.yaml) + --from-ingress-class : 기존 ingressclassname 지정 (지정된 대상만 변환) + --namespace : 지정시 해당 namespace 내 ingress만 변환 ``` # 구성 (Kubernetes 실행 기준) diff --git a/migrate.py b/migrate.py index a7154ed..a8a0c7b 100644 --- a/migrate.py +++ b/migrate.py @@ -20,10 +20,17 @@ yaml.width = 4096 # ----------------------------- # kubectl ingress 조회 # ----------------------------- -def kubectl_ingress(): - out = subprocess.check_output( - ["kubectl", "get", "ingress", "--all-namespaces", "-o", "yaml"] - ) +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) @@ -37,7 +44,6 @@ def load_mapping(path): # ----------------------------- # annotation 렌더링 -# (모든 value를 문자열로 강제) # ----------------------------- def render_annotations_block(unsupported, partial, converted): lines = [] @@ -53,7 +59,7 @@ def render_annotations_block(unsupported, partial, converted): for p in partial: lines.append(f" # PARTIAL SUPPORT: {p['note']}") lines.append(f" {p['haproxy']}: \"{str(p['value'])}\"") - lines.append("") + lines.append("") for c in converted: lines.append(f" {c['haproxy']}: \"{str(c['value'])}\"") @@ -62,7 +68,7 @@ def render_annotations_block(unsupported, partial, converted): # ----------------------------- -# 불필요한 metadata 제거 +# metadata 정리 # ----------------------------- def cleanup_metadata(meta): for k in [ @@ -75,10 +81,46 @@ def cleanup_metadata(meta): 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, ingress_class=None): +def migrate_ingress(data, mapping, to_ingress_class=None): report = { "converted": [], "partial": [], @@ -98,8 +140,8 @@ def migrate_ingress(data, mapping, ingress_class=None): namespace = meta.get("namespace", "default") ingress_id = f"{namespace}/{name}" - if ingress_class: - spec["ingressClassName"] = ingress_class + if to_ingress_class: + spec["ingressClassName"] = to_ingress_class anns = meta.get("annotations", {}) or {} @@ -123,18 +165,13 @@ def migrate_ingress(data, mapping, ingress_class=None): report["converted"].append(k) elif support == "partial" and haproxy_key: - partial.append( - {"haproxy": haproxy_key, "value": v, "note": note} - ) + 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} - ) + unsupported.append({"key": k, "value": v, "note": note}) report["unsupported"].append(k) report["detail"][ingress_id]["unsupported"].append( {"nginx": k, "value": v, "note": note} @@ -182,18 +219,24 @@ def write_report(report, path): # ----------------------------- -# 파일 저장 (split) +# 파일 저장 (yaml/ns/ingress.yaml) # ----------------------------- -def save_split(data, out_dir): - Path(out_dir).mkdir(parents=True, exist_ok=True) +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", "") - name = item["metadata"]["name"] - namespace = item["metadata"].get("namespace", "default") - path = Path(out_dir) / f"{namespace}__{name}.yaml" + meta = item["metadata"] + name = meta["name"] + namespace = meta.get("namespace", "default") - with open(path, "w") as f: + 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: @@ -206,33 +249,6 @@ def save_split(data, out_dir): path.write_text(content) -# ----------------------------- -# 파일 저장 (single) -# ----------------------------- -def save_single(data, out_path): - rendered_map = {} - - for item in data.get("items", []): - rendered_map[id(item)] = item.pop("_rendered_annotations", "") - - path = Path(out_path) - with path.open("w") as f: - yaml.dump(data, f) - - content = path.read_text() - - for item in data.get("items", []): - rendered = rendered_map.get(id(item)) - if rendered: - content = content.replace( - "metadata:\n", - "metadata:\n annotations:\n" + rendered + "\n", - 1, - ) - - path.write_text(content) - - # ----------------------------- # main # ----------------------------- @@ -241,37 +257,55 @@ def main(): 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("--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.single and not args.split: - parser.error("one of --single or --split must be specified") + 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) - ingress = kubectl_ingress() mapping = load_mapping(args.mapping) converted, report = migrate_ingress( - ingress, mapping, ingress_class=args.ingress_class + ingress, + mapping, + to_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) - + 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//.yaml") + print(f"Output : {args.out}//.yaml") print(f"Report : {args.report}")