260 lines
10 KiB
Markdown
260 lines
10 KiB
Markdown
# 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 B1–B4:** 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 B1–B6).
|
||
- 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).
|