From b7d30f2f15ff572fd17a7e1563f14d66a8114ebe Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 21 Sep 2026 19:02:47 +0000 Subject: [PATCH] =?UTF-8?q?publicar-repo:=20el=20vhost,=20con=20validaci?= =?UTF-8?q?=C3=B3n=20y=20vuelta=20atr=C3=A1s=20=E2=80=94=20y=20publicar=20?= =?UTF-8?q?deja=20de=20ser=20compilar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos cosas que el ensayo del guion destapó, las dos medidas: 1. `repo-perfil.sh` NO acotaba el build. `pack --build` es instantáneo con cache-hit y una compilación entera si el artefacto no está sellado, así que «publicá el repo» se convertía en silencio en «ponete a compilar el perfil»: la corrida se puso a moler `libnftnl` en una caja de 4 cores que ya estaba a load 46, y después venía `rust`. `build-repo.sh` llevaba `timeout` desde siempre; el que se corre en producción, no. Ahora BUILD_TIMEOUT (120 s) y SKIP_UNSEALED=1, con el vencimiento tratado como «no estaba sellado» y no como error. Resultado del perfil entero: 173 anclados, 0 sin ancla, 2 saltados (os-release, rust), sin compilar nada. 2. `--install-vhost` pone el bloque de Caddy sin que haya que editar a mano, pero el orden es lo único que lo vuelve seguro: copia → añade → `caddy validate` → SÓLO ENTONCES `reload`. Si validate o reload fallan, restaura la copia y recarga con ella. Un reload con la config rota no tira el sitio nuevo: tira los 19 de la caja. Probado en los dos caminos contra un Caddy de juguete (el de la caja no tiene el admin abierto): con reload bueno, el admin muestra el server nuevo sirviendo el repo y el sitio previo intacto; con reload fallido, el Caddyfile vuelve byte a byte y no queda rastro del bloque. Y `repo-perfil.sh` pasa a respetar TAKANA del entorno: la guarda de publicar-repo.sh comprobaba un binario y se publicaba con otro, que es peor que no tener guarda. --- docs/28-servidor-de-produccion.md | 19 ++++++ scripts/repo-perfil.sh | 25 +++++-- scripts/servidor/publicar-repo.sh | 106 +++++++++++++++++++++++++++--- 3 files changed, 136 insertions(+), 14 deletions(-) diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md index 4045efad..63a589f8 100644 --- a/docs/28-servidor-de-produccion.md +++ b/docs/28-servidor-de-produccion.md @@ -570,6 +570,25 @@ firma en `/srv/repo`, comprueba el lazo y **imprime el bloque sin tocar el Caddy caja Caddy sirve 19 dominios y su config ya acumula 20 `.bak`; un guion que la reescribe a ciegas es cómo se tiran 19 sitios para levantar uno. +⚠ **Publicar no es construir, y `repo-perfil.sh` no lo acotaba.** `pack --build` es instantáneo con +cache-hit y una **compilación entera** si el artefacto no está sellado. Medido al ensayar el guion: +la corrida se puso a compilar `libnftnl` —y después habría seguido con `rust`— en una caja de 4 +cores que ya estaba a **load 46**. `build-repo.sh` llevaba `timeout` desde siempre; el que se corre +en producción, no. Ahora `BUILD_TIMEOUT` (120 s) y `SKIP_UNSEALED=1`: un vencimiento **no es un +error**, significa «ése no estaba sellado». Con eso, el perfil entero publica **173 anclados, 0 sin +ancla, 2 saltados (`os-release`, `rust`)** sin compilar nada. + +**El `--install-vhost`, probado en sus dos caminos** *(con un Caddy de juguete, porque el de la caja +no tiene el admin abierto)*: + +| camino | qué pasó | +|---|---| +| reload OK | `✓ la config valida` → `✓ recargado`; el admin muestra el `srv1` nuevo sirviendo el repo **y el sitio que ya estaba sigue en pie** | +| reload FALLA | copia restaurada **byte a byte** (`b"repo.local" not in` el fichero final), y no recarga nada | + +El orden es lo único que lo hace seguro: copia → añadir → `caddy validate` → **sólo entonces** +`reload`. Un reload con la config rota no tira el sitio nuevo: tira los 19. + ⚠ **Y trae su propia guarda contra el error más fácil de cometer:** si el `takana` de la caja es anterior al §5.5 (se detecta porque no conoce `outdated`), **aborta antes de publicar**. Con uno viejo el repo sale con las 16 recetas multi-parche irreproducibles y 8 paquetes prometiendo un diff --git a/scripts/repo-perfil.sh b/scripts/repo-perfil.sh index 514ee50c..989dd564 100755 --- a/scripts/repo-perfil.sh +++ b/scripts/repo-perfil.sh @@ -13,9 +13,18 @@ # que nadie conoce y que cambia en cada corrida no lo verifica nadie: es decoración (SDD 19 §3.2, # «la firma sin gestión de claves es teatro»). Acá, si no está la clave, se ABORTA. # +# ── PUBLICAR NO ES CONSTRUIR (2026-09-21) ─────────────────────────────────────────────────────── +# `pack --build` es instantáneo si el artefacto ya está sellado (cache-hit) y una COMPILACIÓN +# ENTERA si no lo está. Sin acotarlo, «publicá el repo» se convierte en silencio en «ponete a +# compilar el perfil»: medido acá, la corrida se paró sola en `libnftnl` y se quedó moliendo en una +# caja de 4 cores que ya estaba a load 46. `build-repo.sh` ya llevaba el `timeout`; esto no, y es el +# que se corre en producción. Un vencimiento NO es un error: significa «ése no estaba sellado», y el +# paquete sale sin ancla (o se lo salta, con SKIP_UNSEALED=1). +# # Uso: ./scripts/repo-perfil.sh [perfil] # Env: PERFIL (def servidor) · STORE (def ./store) · REPO (def dist/repo) # KEY (def ~/.config/takana/keys/release.ed25519) · TRUST (def ./trust) · DISTRO (def dev) +# BUILD_TIMEOUT (def 120 s por receta) · SKIP_UNSEALED=1 (no publicar lo que no esté sellado) set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" @@ -25,7 +34,12 @@ REPO="${REPO:-dist/repo}" KEY="${KEY:-$HOME/.config/takana/keys/release.ed25519}" TRUST="${TRUST:-./trust}" DISTRO="${DISTRO:-dev}" -TAKANA="$ROOT/target/release/takana" +BUILD_TIMEOUT="${BUILD_TIMEOUT:-120}" +SKIP_UNSEALED="${SKIP_UNSEALED:-0}" +# Overridable: `publicar-repo.sh` comprueba que el binario traiga el arreglo del SDD 28 §5.5 antes +# de publicar, y esa guarda es mentira si acá se usa otro. Un guion que valida un binario y ejecuta +# otro es peor que uno sin guarda: da confianza sin darla. +TAKANA="${TAKANA:-$ROOT/target/release/takana}" [ -x "$TAKANA" ] || { echo "!! falta $TAKANA (cargo build --release)" >&2; exit 2; } [ -r "$KEY" ] || { @@ -56,12 +70,15 @@ total=$(printf '%s\n' "$LISTA" | grep -c .) echo "==> perfil '$PERFIL': $total paquetes en la clausura → $REPO" rm -rf "$REPO"; mkdir -p "$REPO" -anclados=0; sin_ancla=0; fallaron="" +anclados=0; sin_ancla=0; fallaron=""; saltados="" for n in $LISTA; do r="recipes/$n.toml" [ -r "$r" ] || { fallaron="$fallaron $n(sin-receta)"; continue; } - if "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" --distro-version "$DISTRO" --build >/dev/null 2>&1; then + if timeout "$BUILD_TIMEOUT" "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" \ + --distro-version "$DISTRO" --build >/dev/null 2>&1; then anclados=$((anclados + 1)) + elif [ "$SKIP_UNSEALED" = "1" ]; then + saltados="$saltados $n" elif "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" --distro-version "$DISTRO" >/dev/null 2>&1; then sin_ancla=$((sin_ancla + 1)) else @@ -78,6 +95,6 @@ echo "==> firmando el índice con $KEY" echo "==> ✓ el índice verifica contra $TRUST" echo -echo "publicado: $anclados con expected_hash anclado, $sin_ancla sin ancla.${fallaron:+ FALLARON:$fallaron}" +echo "publicado: $anclados con expected_hash anclado, $sin_ancla sin ancla.${saltados:+ SIN SELLAR (saltados):$saltados}${fallaron:+ FALLARON:$fallaron}" echo "repo firmado en $REPO" echo " consumir: takana install --repo --trust --require-signed" diff --git a/scripts/servidor/publicar-repo.sh b/scripts/servidor/publicar-repo.sh index 18695a67..1025d3c2 100755 --- a/scripts/servidor/publicar-repo.sh +++ b/scripts/servidor/publicar-repo.sh @@ -8,36 +8,55 @@ # 2. comprueba que el índice verifica contra `trust/`, # 3. dice si Caddy ya sirve el nombre, y **si no, imprime el bloque y para**. # -# ── POR QUÉ NO EDITA EL CADDYFILE ─────────────────────────────────────────────────────────────── -# En esta caja Caddy sirve 19 dominios y su Caddyfile ya acumula 20 `.bak` (SDD 28 §1). Un guion que -# lo reescribe a ciegas es cómo se tiran 19 sitios para levantar uno. El bloque va a mano, una vez: +# ── EL CADDYFILE: SÓLO CON `--install-vhost`, Y CON VUELTA ATRÁS ─────────────────────────────── +# Por defecto NO lo toca: imprime el bloque y para. En esta caja Caddy sirve 19 dominios y su +# Caddyfile ya acumula 20 `.bak` (SDD 28 §1); un guion que lo reescribe a ciegas es cómo se tiran +# 19 sitios para levantar uno. # # repo.gioser.net { # root * /srv/repo # file_server browse # } # -# y después `caddy reload --config /etc/caddy/Caddyfile`. El DNS ya está: `repo` A → 2.29.29.217, -# ttl 60, en la zona de Hetzner (dado de alta el 2026-09-21). +# Con `--install-vhost` lo hace, pero **el orden importa y es lo único que lo vuelve seguro**: +# copia de seguridad → añade → `caddy validate` → sólo entonces `caddy reload`. Si validate o +# reload fallan, **restaura la copia y recarga con ella** antes de salir: el estado final es el que +# había, no uno a medias. Y si el nombre ya está en la config, no añade nada (idempotente). +# +# El DNS ya está: `repo` A → 2.29.29.217, ttl 60, en la zona de Hetzner (alta del 2026-09-21). # # ⚠ **Un solo origen es exactamente lo que el ADR 0014 existe para no tener.** Esta caja sirviéndose # a sí misma está bien como camino normal, pero el `--repo` de los clientes debería llevar también # el Storage Box: el primer origen roto, si es el único, es el último. # # Uso: sudo scripts/servidor/publicar-repo.sh [perfil] +# sudo scripts/servidor/publicar-repo.sh --install-vhost [perfil] # Env: PERFIL (def servidor) · STORE (def /store) · DESTINO (def /srv/repo) -# NOMBRE (def repo.gioser.net) · TRUST (def ./trust) +# NOMBRE (def repo.gioser.net) · TRUST (def ./trust) · CADDYFILE (def /etc/caddy/Caddyfile) set -eu ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" +INSTALL_VHOST=0 +case "${1:-}" in --install-vhost) INSTALL_VHOST=1; shift ;; esac + PERFIL="${1:-${PERFIL:-servidor}}" STORE="${STORE:-/store}" DESTINO="${DESTINO:-/srv/repo}" NOMBRE="${NOMBRE:-repo.gioser.net}" TRUST="${TRUST:-$ROOT/trust}" TAKANA="${TAKANA:-$ROOT/target/release/takana}" +CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}" -[ "$(id -u)" = "0" ] || { echo "!! corré esto como root: la clave de release está en /root/.config/takana/keys/" >&2; exit 2; } +# La guarda pregunta por lo que de verdad hace falta —poder LEER la clave— y no por `id -u == 0`, +# que es sólo su causa habitual: así el guion se puede ensayar contra un repo y una clave de +# prueba sin ser root, y el día que la clave viva en otro sitio no hay que tocar la condición. +KEY="${KEY:-$HOME/.config/takana/keys/release.ed25519}" +[ -r "$KEY" ] || { + echo "!! no puedo leer la clave de release: $KEY" >&2 + echo " En la caja vive en /root/.config/takana/keys/ ⇒ esto va como root (SDD 28 §2116)." >&2 + exit 2 +} +export KEY [ -x "$TAKANA" ] || { echo "!! falta $TAKANA (cargo build --release)" >&2; exit 2; } # ⚠ El binario TIENE que traer el arreglo de los parches múltiples y del target_bin (SDD 28 §5.5): @@ -51,7 +70,56 @@ TAKANA="${TAKANA:-$ROOT/target/release/takana}" echo "==> publicando el perfil '$PERFIL' en $DESTINO" install -d -m 755 "$DESTINO" -PERFIL="$PERFIL" STORE="$STORE" REPO="$DESTINO" TRUST="$TRUST" "$ROOT/scripts/repo-perfil.sh" "$PERFIL" +PERFIL="$PERFIL" STORE="$STORE" REPO="$DESTINO" TRUST="$TRUST" TAKANA="$TAKANA" \ + "$ROOT/scripts/repo-perfil.sh" "$PERFIL" + +if [ "$INSTALL_VHOST" = "1" ]; then + echo + echo "==> vhost de Caddy en $CADDYFILE" + [ -w "$CADDYFILE" ] || { echo "!! no puedo escribir $CADDYFILE" >&2; exit 2; } + command -v caddy >/dev/null || { echo "!! no encuentro el binario 'caddy' en el PATH" >&2; exit 2; } + + if grep -qE "^[[:space:]]*(https?://)?$NOMBRE[[:space:],{]" "$CADDYFILE"; then + echo " = $NOMBRE ya está en la config; no toco nada" + else + COPIA="$CADDYFILE.antes-de-$NOMBRE-$(date -u +%Y%m%dT%H%M%SZ)" + cp -p "$CADDYFILE" "$COPIA" + echo " copia de seguridad: $COPIA" + + # Un site block va al FINAL: las opciones globales de Caddy tienen que ser lo primero del + # fichero, así que añadir al final nunca las invalida. + cat >> "$CADDYFILE" </dev/null 2>&1; then + cp -p "$COPIA" "$CADDYFILE" + echo "!! la config NO valida con el bloque añadido — restaurada $COPIA, no recargué" >&2 + caddy validate --config "$CADDYFILE" >/dev/null 2>&1 \ + || echo "!! y la ORIGINAL tampoco valida: eso ya venía roto, miralo antes de recargar" >&2 + exit 1 + fi + echo " ✓ la config valida" + + if ! caddy reload --config "$CADDYFILE"; then + # El reload falló: Caddy sigue con lo que tenía cargado, pero el fichero en disco ya no + # es eso. Se restaura para que el próximo arranque no levante una config que no probó + # nadie, y se reintenta el reload con la buena. + cp -p "$COPIA" "$CADDYFILE" + caddy reload --config "$CADDYFILE" >/dev/null 2>&1 || true + echo "!! el reload falló — restaurada $COPIA y recargada" >&2 + exit 1 + fi + echo " ✓ recargado" + fi +fi echo echo "==> ¿lo sirve Caddy?" @@ -63,16 +131,34 @@ if curl -fsS -m 10 "https://$NOMBRE/index.json" -o /dev/null 2>/dev/null; then echo " y el aviso periódico:" echo " REPO=https://$NOMBRE scripts/servidor/avisar-actualizaciones.sh" else + if [ "$INSTALL_VHOST" = "1" ]; then + # Decir «falta el vhost» justo después de haberlo puesto es la clase de mensaje que manda a + # buscar el problema donde no está. + cat <