Un unit de systemd no es un sitio web. Meterlo en `Sitio`/`Ruta` sería justo el «parecerse» que el acta existe para evitar, así que cada familia tiene su modelo y el centro empareja SÓLO dentro de la familia. Un par cruzado se rechaza con un error, no con un intento: `systemd → caddy` sale por `sys.exit`, porque traducir entre familias daría un fichero que PARECE correcto. Dos familias hoy: `web` (nginx, apache → caddy) y `servicio` (systemd → arje). 3 pares con 5 plugins. **Dónde una elisión silenciosa no pierde una opción sino que CAMBIA el sistema.** El payload `Native` de arje acepta `exec`, `argv` y `envp` y nada más (comprobado en la semilla real del producto): NO hay campo de usuario. Un `User=git` traducido en silencio correría el servicio COMO ROOT — escalación de privilegios en un fichero generado que nadie vuelve a leer. Sale SIN-TRADUCIR con su línea. Igual `EnvironmentFile=` (el fichero no está en la máquina donde se traduce ⇒ envp incompleto, falla tarde), `Type=forking` (arje supervisa al proceso que lanza ⇒ BUCLE DE REINICIO) y todo el endurecimiento, que callado entrega un servicio MENOS confinado que el original. Lo que sí tiene destino exacto: `Type=oneshot` → `"supervision": "OneShot"`, que ya existe en la semilla del producto — buscarlo antes de declararlo intraducible evitó una elisión inventada. **Lector y escritor no opinan del mismo campo.** `User=` lo CAPTURA el lector y lo JUZGA el escritor; cuando los dos anotaban, el acta decía dos cosas distintas de la misma línea, y un acta que se contradice se deja de leer. El lector registra en qué línea vio cada campo (`Servicio.lineas`) para que el escritor señale la línea real en vez de un 0. Controles: la tarjeta generada tiene el MISMO juego de claves que la tarjeta real de `sshd` del producto (+`_mudanza` de procedencia); dos corridas dan el fichero byte a byte idéntico; y la familia `web` sigue validando con el caddy del corpus (`Valid configuration` en los dos pares). De paso, `ulid_determinista` sale a `ids.py`: lo necesitaban `declarar.py` y el escritor de arje, y copiarlo habría dejado dos generadores de id que se pueden separar sin que nada falle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
75 lines
3.4 KiB
Python
75 lines
3.4 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", familia="web")
|
|
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"
|