ligbox-ops-platform/specs/026-purge-traefik-validation/spec.md
Ligbox Spec Hub 3a2c64834b Initial import: ligbox-ops-platform + specs + LAPTOP + obsidian merge (CT130)
Source: VM122 /opt + obsidian-infra + LAPTOP
Hub: CT130 spec-hub 10.10.10.130
2026-06-19 17:26:41 +00:00

260 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Spec 026 — Purge VM112: validação Traefik pós-remoção (CT114)
**Criado:** 2026-06-19
**Solicitado por:** Roger
**Prioridade:** **P0** (incidente produção)
**Status:** ✅ Implementado (VM112 + CT114, 2026-06-19)
**Sistema:** Wizard VM112 · Traefik CT114 · Desk VM122
**Relacionado:** Spec **017** (purge domínio) · Spec **025** (continuidade wizard) · Spec **018** (Serviços / drawer purge)
---
## Incidente que motivou a spec
**Data:** 2026-06-19 ~02:18 UTC
**Sintoma:** `https://onboard.ligbox.com.br/onboard`**404 page not found** (Traefik Go default), afectando **todos** os domínios onboard — não só o domínio purgado.
**Domínios purgados na sessão:** `iofficebooks.com`, `exuberanti.com.br`.
**Causa raiz:**
1. `_purge_traefik_routers()` em `/opt/ligbox-wizard/backend/app/services/domain_orchestration.py` remove routers por **corte de texto** (`Host(...)` → próximo `\n `).
2. Isso deixou bloco **`mail-mail-exuberanti-com-br-Router` sem `rule:`** e **chave duplicada** no `dynamic.yml`.
3. Traefik v3.6 **rejeitou o ficheiro inteiro**:
```
yaml: unmarshal errors: mapping key "mail-mail-exuberanti-com-br-Router" already defined
```
4. Após restart, só **3 routers internos** activos (`acme`, `api`, `dashboard`) — zero rotas de produção.
5. O purge reportou **`traefik_ok`** porque validou apenas **SSH write + restart**, não carga efectiva da config.
**Correcção manual aplicada (19/06):** remoção de routers inválidos/duplicados + restart Traefik → 62 routers activos.
---
## Objetivo
Tornar o purge de domínio **seguro para a plataforma inteira**: após remover um tenant, o Traefik **tem de continuar operacional** e o onboard **tem de responder 200**.
**Regra de ouro (nova):**
> Purge só está **concluído** quando o domínio sumiu da base **e** o Traefik tem ≥ N routers **e** `GET https://onboard.ligbox.com.br/onboard` → **200** com HTML do wizard.
---
## Fora de escopo
- Reescrever Spec 017 (histórico Desk, RBAC, drawer)
- Purge parcial (só DNS, só contas)
- Validação de certificados LE por domínio purgado (opcional futuro)
- Automatizar purge agendado
---
## Problema na implementação actual (VM112)
| Função | Ficheiro | Problema |
|--------|----------|----------|
| `_purge_traefik_routers` | `domain_orchestration.py` | Corte textual frágil; não remove middleware `webmail-pending-{slug}`; não valida YAML |
| `_purge_traefik_sni` | idem | OK funcional; falta verificação pós-restart HAProxy |
| `_execute_purge` | idem | Marca `traefik_ok` sem smoke test |
| Portal users | `_purge_portal_users` | Só `/var/lib/ibytera-mail-portal/portal_users/`**falta** `/var/lib/ligbox-wizard/portal_users/` |
| Nginx Carbonio | — | **Não** limpa vhosts `mail.{domain}` em `/opt/zextras/conf/nginx/includes/` |
| Branding / scripts deploy | — | **Não** remove entrada `tenant_branding.py` nem refs em `apply-admin-nginx-overrides.py` |
---
## Solução proposta
### Fase A — Remoção Traefik robusta (P0)
Substituir corte textual por script Python remoto no CT114 (mesmo padrão de `infrastructure.do_traefik()`):
1. **Backup** antes de editar:
```
/root/traefik/dynamic.yml.bak-purge-{domain_slug}-{timestamp}
```
2. **Parse YAML** (`yaml.safe_load` para validação; **edição linha-a-linha** — nunca `safe_dump` no ficheiro inteiro).
3. Remover, por domínio:
- Router `mail-mail-{slug}-Router` (e variantes)
- Middleware `webmail-pending-{slug}` (redirect regex para wizard)
- Qualquer router cujo `rule` contenha `Host(\`mail.{domain}\`)` ou alias mail
4. **Validação pré-restart:**
- YAML parse OK
- Zero chaves duplicadas em `http.routers`
- Todo router tem campo `rule` não vazio
- Zero ocorrências de `mail.{domain}` no texto (sanity grep)
5. **Restart** Traefik só se validação OK.
6. Se validação falhar → **rollback** do backup **sem** restart.
**Slug:** `{domain}` com `.``-` (ex.: `exuberanti.com.br``exuberanti-com-br`).
---
### Fase B — Verificação pós-purge (P0)
Novo step `_execute_purge`: **`traefik_validate`** (após `traefik_routers`).
| # | Check | Comando / origem | Critério |
|---|-------|------------------|----------|
| B1 | Routers carregados | `curl -s http://127.0.0.1:8080/api/http/routers` (CT114) | `count ≥ 10` (alerta se `< 10`; falha se `< 5`) |
| B2 | Onboard router activo | JSON routers | existe `onboard-ligbox-Router@file` enabled |
| B3 | Smoke HTTPS onboard | `curl -sf -o /dev/null -w '%{http_code}' https://onboard.ligbox.com.br/onboard` | `200` |
| B4 | Smoke API VM112 | `curl -sf -o /dev/null -w '%{http_code}' http://10.10.10.112:8090/onboard` | `200` |
| B5 | Sem refs domínio no dynamic | `grep -i {domain}` em `dynamic.yml` | 0 matches (excepto backup) |
| B6 | Log Traefik limpo | `docker logs traefik 2>&1 \| tail -20` | sem `unmarshal errors` / `invalid rule` nos últimos 30s |
**Falha em B1B4:** rollback `dynamic.yml` + restart Traefik + step `traefik_validate` = **error** + job purge = **error** (não `done`).
**Timeline Desk:** novo passo visível «Validar Traefik / onboard» com detalhe de cada check.
---
### Fase C — Purge VM112 completo (P1)
Expandir `_execute_purge` com steps adicionais (ou sub-steps documentados):
| Step | Acção |
|------|--------|
| `portal_users_wizard_store` | Apagar JSON em `/var/lib/ligbox-wizard/portal_users/` cujo email ∈ domínio |
| `nginx_vhosts` | Remover `server_name mail.{domain}` de includes nginx Carbonio + `nginx -t` + reload |
| `tenant_branding` | Remover linha em `tenant_branding.py` |
| `deploy_scripts` | Remover `mail.{domain}` de `apply-admin-nginx-overrides.py` e `sync-traefik-admin-certs.sh` |
| `traefik_export_certs` | Apagar `mail-{slug}*.pem` em `/opt/zextras/ssl/letsencrypt/traefik-export/` |
Cada step reporta `ok` / `error` na timeline; falha nginx `nginx -t`**error** (não deixa mail quebrado).
---
### Fase D — Desk / histórico (P2)
- Persistir em `vm112_json` do job: resultado de `traefik_validate` (checks B1B6).
- Badge **error** no histórico se rollback Traefik ocorreu.
- Alerta ops (ntfy / webhook) quando purge falha em `traefik_validate`.
---
## Alterações de API / timeline
### VM112 — novos steps em `POST /api/admin/domains/{domain}/purge`
Ordem actualizada (trecho Traefik):
```
traefik_sni → running → done|error
traefik_routers → running → done|error (Fase A — lógica nova)
traefik_validate → running → done|error (Fase B — NOVO)
```
**Resposta `result` (campos novos):**
```json
{
"traefik_validate": {
"ok": true,
"router_count": 62,
"onboard_http": 200,
"rollback": false
},
"traefik_rollback": null
}
```
Em falha:
```json
{
"traefik_validate": { "ok": false, "router_count": 3, "onboard_http": 404, "rollback": true },
"traefik_rollback": "dynamic.yml.bak-purge-exuberanti-com-br-20260619T021800Z"
}
```
---
## Ficheiros a alterar
| VM | Ficheiro | Fase |
|----|----------|------|
| 112 | `backend/app/services/domain_orchestration.py` | A, B, C |
| 112 | `backend/app/services/infrastructure.py` | A (reutilizar `_router_key_for_host`, SSH helpers) |
| 114 | `/root/traefik/dynamic.yml` | _(runtime — só via purge script)_ |
| 122 | `api/app/vm112_domains_routes.py` | D (opcional — repassar novos campos) |
| 122 | `frontend/assets/app.js` | D (render checks no modal histórico) |
**Deploy:** VM112 `systemctl restart ligbox-wizard` após merge.
---
## Critérios de aceitação
1. Purge de domínio teste remove router/middleware **sem** duplicar chaves YAML.
2. Após purge, Traefik API reporta **≥ 10** routers HTTP.
3. `curl -sf https://onboard.ligbox.com.br/onboard`**200** imediatamente após purge.
4. Purge com YAML inválido simulado → **rollback** automático + job status **error** (não `done`).
5. Portal users removidos de **ambas** as pastas (`ibytera-mail-portal` + `ligbox-wizard`).
6. Histórico Desk (Spec 017 v2) mostra step `traefik_validate` com detalhe.
7. Regressão: purge de domínio inexistente (`no_zone`, `domínio já ausente`) continua idempotente.
---
## Test plan (E2E)
```bash
# Pré: criar domínio teste via wizard (zona CF pending OK)
DOMAIN=teste-purge-$(date +%s).example.com # ou domínio real de lab
# Executar purge
curl -s -X POST "http://10.10.10.112:8090/api/admin/domains/${DOMAIN}/purge?sync=true" \
-H "X-Api-Key: $ADMIN_API_KEY" | jq '.result.traefik_validate'
# Validar plataforma
curl -sf -o /dev/null -w "onboard:%{http_code}\n" https://onboard.ligbox.com.br/onboard
ssh root@10.10.10.114 'curl -s http://127.0.0.1:8080/api/http/routers | python3 -c "import sys,json; print(len(json.load(sys.stdin)),\"routers\")"'
```
**Teste de regressão (incidente 19/06):** purge `exuberanti.com.br` duas vezes seguidas → segunda execução idempotente, Traefik estável.
---
## Riscos e mitigação
| Risco | Mitigação |
|-------|-----------|
| Rollback falha | Manter últimos 5 backups `dynamic.yml.bak-purge-*` |
| Traefik API :8080 fechado externamente | Checks só via SSH CT114 localhost |
| Purge longo (>60s) | Jobs async Spec 017 já existem; validate no final |
| Race: dois purges simultâneos | Lock file CT114 `/tmp/traefik-dynamic.lock` |
---
## Prioridade no backlog
| Fase | Prioridade | Motivo |
|------|------------|--------|
| **A + B** | **P0** | Evita outage total do onboard |
| **C** | P1 | Limpeza completa tenant (nginx, branding) |
| **D** | P2 | Observabilidade Desk |
---
## Referências
- Incidente: purge `exuberanti.com.br` 2026-06-19 — Traefik 3 routers only
- Spec 017 — ordem purge VM112 + histórico Desk
- Spec 025 — item backlog «Traefik YAML validation» (consolidar implementação aqui)
- Log Traefik: `mapping key "mail-mail-exuberanti-com-br-Router" already defined at line 475`
- Fix manual: `dynamic.yml.bak-fix-dup-exuberanti-20260619`
---
## Conclusão (estado actual)
| Fase | Entrega | Estado |
|------|---------|--------|
| A | Remoção YAML estruturada + backup/rollback | ✅ |
| B | `traefik_validate` + smoke onboard | ✅ |
| C | Purge nginx / branding / wizard store | ✅ (parcial — VM112) |
| D | Histórico Desk + alerta ops | 📋 |
**Implementação pendente em VM112** — esta spec documenta o backlog acordado com Roger (2026-06-19).