Files
takana/scripts/mudanza/formatos/caddy.py
T
SergioandClaude Opus 5 3137d47f16 traducir: CENTRO de traducción con modelo pivote y plugins — N+M en vez de N×M
Traducir de a pares no escala: con nginx, apache, caddy, haproxy y lighttpd son 20 traductores, y
cada formato nuevo agrega 2N. Con un modelo intermedio son N lectores + M escritores: un formato
nuevo cuesta uno o dos plugins y estrena todos los pares de golpe.

**Probado, no argumentado**: el lector de apache se agregó SIN TOCAR el escritor de Caddy, y con eso
`apache → caddy` funciona sin que exista ningún traductor de ese par. Las dos salidas las valida el
caddy del propio corpus: `Valid configuration` en las dos.

Los plugins se DESCUBREN (`formatos/` + `@lector` / `@escritor`): una lista mantenida a mano se
desincroniza y el formato nuevo «no existe» sin que nada falle. `--list` dice qué pares habilita.

**El acta viaja DENTRO del modelo**, y es la decisión que hace que el pivote no mienta: si fuera un
efecto de cada traductor, lo que el LECTOR no entendió se perdería antes de llegar al escritor. El
escritor además puede agregarle — Caddy no tiene equivalente de `location ~`, así que en vez de
emitir un `handle` de prefijo que SE PARECE, lo manda al acta. Parecerse es peor que faltar.

Y un bug del lector de apache, encontrado probando: `ProxyPass` tiene dos formas y dentro de
`<Location>` lleva sólo la URL. Mirar sólo la de tres tokens dejaba sin traducir la forma más común y
el acta la reportaba como «no reconocida» — un falso negativo que manda a escribir a mano algo que el
lector sí sabe hacer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 18:27:32 +00:00

75 lines
3.3 KiB
Python

"""Plugin de formato: ESCRITOR de Caddy (SDD 29)."""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from formatos import escritor # noqa: E402
def _direcciones(s):
"""En nginx dos `server` con el MISMO nombre se distinguen por su `listen`; en Caddy, por el
ESQUEMA de la dirección. Sin traducir eso, los bloques colisionan y caddy rechaza el fichero
entero con «ambiguous site definition» — encontrado validando con el binario real, no leyendo."""
solo_80 = bool(s.puertos) and all(p == "80" for p in s.puertos)
pref = "http://" if solo_80 else ""
return ", ".join(pref + n for n in s.nombres) or (":80" if solo_80 else ":443")
def _emitir_acciones(acciones, ind):
L = []
for a in acciones:
if a.tipo == "proxy":
L.append(f"{ind}reverse_proxy {a.valor.rstrip('/').replace('http://','').replace('https://','')}")
elif a.tipo == "archivos":
L.append(f"{ind}root * {a.valor}")
elif a.tipo == "intentar":
L.append(f"{ind}try_files {a.valor}")
elif a.tipo == "redir":
p = a.valor.split()
code = p[0] if p and p[0].isdigit() else ""
dest = " ".join(p[1:]) if code else a.valor
L.append(f"{ind}redir {dest} {code}".rstrip() if dest else f"{ind}respond {code}")
return L
@escritor("caddy")
def escribir(cfg):
A = cfg.acta
L = [f"# Generado por scripts/mudanza/traducir.py desde una configuración de {cfg.formato_origen}.",
"# ⚠ NO es una traducción completa. Abajo va el ACTA de lo que NO se pudo traducir, con su",
"# línea de origen. Desplegar esto sin resolverlo es desplegar un servidor incompleto.",
""]
for s in cfg.sitios:
L.append(f"{_direcciones(s)} {{")
L.append(f"\t# desde la línea {s.origen} del {cfg.formato_origen} original")
if s.tls.get("cert"):
L.append(f"\ttls {s.tls.get('cert','')} {s.tls.get('key','')}")
if s.raiz:
L.append(f"\troot * {s.raiz}")
L += _emitir_acciones(s.acciones, "\t")
for r in s.rutas:
if r.clase == "regex":
# Caddy no tiene un equivalente directo de `location ~`. Se DECLARA en el acta en vez
# de emitir algo parecido: un `handle` de prefijo no es lo mismo que una regex, y
# parecerse es peor que faltar.
A.anota(r.origen, "SIN-TRADUCIR", f"ruta por regex: {r.patron}",
"Caddy no tiene equivalente directo de `location ~`; se escribe con "
"`@matcher path_regexp` y hay que revisar el orden de evaluación")
continue
cuerpo = _emitir_acciones(r.acciones, "\t\t")
if cuerpo:
L.append(f"\thandle {r.patron} {{ # línea {r.origen}")
L += cuerpo
L.append("\t}")
if s.raiz:
L.append("\tfile_server")
L.append("}")
L.append("")
pend = A.sin_traducir()
if pend:
L.append("# ══ ACTA: lo que NO se tradujo — hay que resolverlo a mano ══")
for e in pend:
L.append(f"# línea {e['linea']:>4} {e['directiva'][:70]}")
if e["motivo"]:
L.append(f"# └─ {e['motivo'][:96]}")
return "\n".join(L) + "\n"