docs: por que la config es como es, y fuera el CI muerto
El README describia un flujo que no existe: ./build.sh con Kaniko y un workflow de Gitea Actions. Kaniko y Gitea Actions se estan retirando, y ademas este workflow llevaba roto desde mayo -- era el unico del arbol que usaba actions/checkout@v4, que necesita node, y el runner :host no lo tiene. Fallaba en 70 s en cada push sin que nadie lo mirara: la imagen que sostuvo el correo 70 dias era del 20 de mayo. Un CI que falla siempre y en silencio es peor que no tener CI, asi que se borra en vez de dejarlo de adorno. Se documenta el build con mcp-build (con el gotcha del image_name sin host, que duplica el registro y muere con 401) y, sobre todo, POR QUE cada linea de la config es la que es. Las cuatro que no se pueden tocar sin reabrir el incidente llevan su sintoma exacto al lado, para que el siguiente que las vea raras sepa lo que cuesta "limpiarlas": tlsmgr, el grupo sasl, el realm desde RELAY_AUTH_DOMAIN y chroot=n. Y queda escrito como se prueba un cambio de esta imagen ANTES de desplegarlo, que aqui no es opcional: desplegar es reiniciar el unico pod que sostiene el correo, y si falla no hay alerta que lo diga porque la alerta ES el correo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2d7863bc01
commit
6a10eab342
@ -1,23 +0,0 @@
|
|||||||
name: Build smtp-relay
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'Dockerfile'
|
|
||||||
- 'entrypoint.sh'
|
|
||||||
- 'main.cf'
|
|
||||||
- 'master.cf'
|
|
||||||
- 'smtpd.conf'
|
|
||||||
- 'scripts/build.sh'
|
|
||||||
- '.gitea/workflows/build.yml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: [self-hosted, valhalla]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build image (dev)
|
|
||||||
run: bash scripts/build.sh dev
|
|
||||||
50
README.md
50
README.md
@ -12,12 +12,22 @@ Deployed on: hermes (`clusters/hermes/smtp-relay/` in asgard)
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
Con **mcp-build** (2026-07-29). Antes había un `scripts/build.sh` con Kaniko y un
|
||||||
./build.sh 1.0.0
|
`.gitea/workflows/build.yml`: **los dos se han borrado**. Kaniko y Gitea Actions se están
|
||||||
|
retirando, y este workflow además llevaba roto desde mayo — era el único del árbol que usaba
|
||||||
|
`actions/checkout@v4`, que necesita `node`, y el runner `:host` no lo tiene
|
||||||
|
(`Cannot find: node in PATH`). Fallaba en 70 s en cada push y **nadie se enteraba**: la imagen que
|
||||||
|
sostuvo el correo durante 70 días era del 20 de mayo.
|
||||||
|
|
||||||
|
```
|
||||||
|
trigger_build(repo_url="https://git.manabo.org/xavor/smtp-relay.git",
|
||||||
|
ref="main", image_tag="dev", image_name="library/smtp-relay")
|
||||||
```
|
```
|
||||||
|
|
||||||
Packages the Dockerfile context, uploads to MinIO, runs Kaniko in-cluster on valhalla,
|
⚠️ `image_name` va **sin el host**. Con `harbor.manabo.org/library/smtp-relay` el destino sale
|
||||||
and pushes the resulting image to Harbor.
|
duplicado (`harbor.manabo.org/harbor.manabo.org/...`) y el push muere con `401`.
|
||||||
|
|
||||||
|
El despliegue se pinea a mano en `asgard/clusters/hermes/smtp-relay/kustomization.yaml`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -61,3 +71,35 @@ Wildcard cert for `relay.manabo.org` — pushed to Vault via PushSecret on valha
|
|||||||
`saslpasswd2` re-creates the sasldb. This is intentional (stateless SASL).
|
`saslpasswd2` re-creates the sasldb. This is intentional (stateless SASL).
|
||||||
- **No DKIM**: DKIM signing is not implemented in this image. Relay delivers mail
|
- **No DKIM**: DKIM signing is not implemented in this image. Relay delivers mail
|
||||||
as-is; DKIM signatures must be added by the sending MTA (Mailu).
|
as-is; DKIM signatures must be added by the sending MTA (Mailu).
|
||||||
|
|
||||||
|
### Por qué la config es como es (incidente del 2026-07-29)
|
||||||
|
|
||||||
|
El correo saliente de `manabo.org` estuvo **semanas sin salir** por cinco fallos encadenados, cada
|
||||||
|
uno tapando al siguiente. Cuatro de los arreglos vivieron 70 días aplicados a mano sobre el
|
||||||
|
contenedor vivo, fuera de git. Están en la imagen desde el 2026-07-29. Relato completo en
|
||||||
|
`asgard/clusters/hermes/smtp-relay/README.md`. **Lo que no se puede tocar sin reabrirlo:**
|
||||||
|
|
||||||
|
- **`tlsmgr` en `master.cf`.** Sin ese daemon, `smtpd` **anuncia** `STARTTLS` en el `EHLO` y luego
|
||||||
|
no puede hacerlo: `454 4.7.0 TLS not available due to local problem`. La capacidad anunciada no
|
||||||
|
prueba la capacidad.
|
||||||
|
- **`postfix` en el grupo `sasl`** (`usermod -aG sasl postfix` en el Dockerfile). `/etc/sasldb2` es
|
||||||
|
`root:sasl 0640`; sin el grupo, `smtpd` no puede abrirlo → `454 Temporary authentication failure`.
|
||||||
|
- **`smtpd_sasl_local_domain` sale del `entrypoint`, no de `main.cf`.** Tiene que ser el mismo
|
||||||
|
`RELAY_AUTH_DOMAIN` con el que `saslpasswd2` crea la entrada del sasldb. Estaba declarado aparte
|
||||||
|
como `$myhostname`, así que el realm era `relayuser@relay.manabo.org` y la entrada real
|
||||||
|
`relayuser@manabo.org`. **No lo devuelvas a `main.cf`**: separados, vuelven a poder divergir.
|
||||||
|
- **`chroot = n` en todos los servicios de `master.cf`.** Con chroot, Postfix necesita
|
||||||
|
`/var/spool/postfix/etc/resolv.conf` poblado en el build; sin él **no resuelve MX** y el correo se
|
||||||
|
queda en cola con `Name service error`, aunque `getent hosts` dentro del contenedor sí resuelva
|
||||||
|
(la herramienta de diagnóstico no ve lo mismo que el consumidor). En un contenedor el chroot no
|
||||||
|
aporta nada: el contenedor **es** el jail.
|
||||||
|
- **`postlogd` + `maillog_file`.** Postfix escribe a syslog y aquí no corre ninguno ⇒ `kubectl logs`
|
||||||
|
sale **vacío**. Sin esto, el incidente se diagnostica a ciegas — y es lo que permite ver si un
|
||||||
|
despliegue futuro sale mal.
|
||||||
|
|
||||||
|
**Cómo se prueba un cambio de esta imagen antes de desplegarlo** (obligatorio: desplegar aquí es
|
||||||
|
reiniciar el único pod que sostiene el correo, y si falla **no hay alerta que lo diga, porque la
|
||||||
|
alerta es el correo**): arranca la imagen candidata aparte con el entrypoint real y las env de
|
||||||
|
producción, y exige `diff` vacío de `postconf -n` y `postconf -M` contra el contenedor en marcha.
|
||||||
|
Un diff limpio no basta: ejercita también `STARTTLS` y `AUTH PLAIN` contra ella (`235
|
||||||
|
Authentication successful`), que es lo que un diff de configuración no caza.
|
||||||
|
|||||||
106
scripts/build.sh
106
scripts/build.sh
@ -1,106 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# scripts/build.sh [tag]
|
|
||||||
# Empaqueta el contexto, sube a MinIO, lanza Kaniko en-cluster, espera.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
TAG="${1:-dev}"
|
|
||||||
HARBOR="harbor.manabo.org"
|
|
||||||
IMAGE="${HARBOR}/library/smtp-relay:${TAG}"
|
|
||||||
BUCKET="kaniko-builds"
|
|
||||||
CONTEXT_KEY="smtp-relay/context.tar.gz"
|
|
||||||
|
|
||||||
echo "==> Building ${IMAGE}"
|
|
||||||
|
|
||||||
echo "==> Packaging context ..."
|
|
||||||
tar -czf /tmp/kaniko-context.tar.gz \
|
|
||||||
--exclude='.git' \
|
|
||||||
--exclude='scripts' \
|
|
||||||
--exclude='.gitea' \
|
|
||||||
--exclude='README.md' \
|
|
||||||
-C "$(git rev-parse --show-toplevel)" \
|
|
||||||
Dockerfile entrypoint.sh main.cf master.cf smtpd.conf
|
|
||||||
|
|
||||||
echo "==> Uploading to MinIO ..."
|
|
||||||
mc cp /tmp/kaniko-context.tar.gz "minio/${BUCKET}/${CONTEXT_KEY}"
|
|
||||||
rm /tmp/kaniko-context.tar.gz
|
|
||||||
|
|
||||||
JOB_NAME="kaniko-smtp-relay-$(date +%s)"
|
|
||||||
echo "==> Launching Kaniko job: ${JOB_NAME}"
|
|
||||||
|
|
||||||
cat <<EOF | kubectl apply -f -
|
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: Job
|
|
||||||
metadata:
|
|
||||||
name: ${JOB_NAME}
|
|
||||||
namespace: kaniko
|
|
||||||
spec:
|
|
||||||
backoffLimit: 0
|
|
||||||
ttlSecondsAfterFinished: 300
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: harbor-pull-secret
|
|
||||||
initContainers:
|
|
||||||
- name: fetch-context
|
|
||||||
image: harbor.manabo.org/library/minio/mc:RELEASE.2025-08-13T08-35-41Z
|
|
||||||
command: ["/bin/sh", "-c"]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
mc alias set minio \$MINIO_ENDPOINT \$MC_ACCESS_KEY \$MC_SECRET_KEY --api S3v4 &&
|
|
||||||
mc cp minio/${BUCKET}/${CONTEXT_KEY} /context/context.tar.gz
|
|
||||||
env:
|
|
||||||
- name: MC_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: minio-kaniko-creds
|
|
||||||
key: access-key
|
|
||||||
- name: MC_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: minio-kaniko-creds
|
|
||||||
key: secret-key
|
|
||||||
- name: MINIO_ENDPOINT
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: minio-kaniko-creds
|
|
||||||
key: endpoint
|
|
||||||
volumeMounts:
|
|
||||||
- name: context
|
|
||||||
mountPath: /context
|
|
||||||
containers:
|
|
||||||
- name: kaniko
|
|
||||||
image: harbor.manabo.org/gcr/kaniko-project/executor:v1.23.2
|
|
||||||
args:
|
|
||||||
- "--context=tar:///context/context.tar.gz"
|
|
||||||
- "--destination=${IMAGE}"
|
|
||||||
- "--snapshot-mode=redo"
|
|
||||||
- "--log-format=text"
|
|
||||||
volumeMounts:
|
|
||||||
- name: context
|
|
||||||
mountPath: /context
|
|
||||||
- name: docker-config
|
|
||||||
mountPath: /kaniko/.docker/
|
|
||||||
volumes:
|
|
||||||
- name: context
|
|
||||||
emptyDir: {}
|
|
||||||
- name: docker-config
|
|
||||||
secret:
|
|
||||||
secretName: harbor-push-config
|
|
||||||
items:
|
|
||||||
- key: .dockerconfigjson
|
|
||||||
path: config.json
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "==> Waiting for build (timeout 10m) ..."
|
|
||||||
kubectl wait "job/${JOB_NAME}" -n kaniko \
|
|
||||||
--for=condition=complete \
|
|
||||||
--timeout=600s || {
|
|
||||||
echo "==> Build FAILED. Logs:"
|
|
||||||
POD=$(kubectl get pods -n kaniko -l "job-name=${JOB_NAME}" -o name | head -1)
|
|
||||||
kubectl logs -n kaniko "$POD" --all-containers
|
|
||||||
kubectl delete "job/${JOB_NAME}" -n kaniko --ignore-not-found
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "==> Done: ${IMAGE}"
|
|
||||||
Loading…
Reference in New Issue
Block a user