O crocha na lista de dominios lia-se da coluna dns_status, e nada lhe escrevia depois do onboarding. A verificacao corre no servico de protocolos, que tem o resolvedor, e a resposta ia directa para o browser sem ficar registada -- por isso um dominio com 13 de 13 registos publicados e a resolver continuava listado como PENDING, ultima verificacao "Nunca", ao lado da pagina que dizia o contrario. Quem verificou passa a escrever o que encontrou. Falhar a gravar nao transforma uma verificacao boa num erro para o operador: fica no log. A coluna aceita apenas PENDING, VERIFIED, PARTIAL, DRIFT e ERROR, e o verificador tambem diz MISSING e UNKNOWN. Escrever qualquer uma dessas violava a check constraint, a actualizacao era rejeitada e o crocha ficava velo na mesma -- exactamente o sintoma que isto vem corrigir. Ha um teste so para essa traducao. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
2.4 KiB
Go
80 lines
2.4 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// DomainRepository lists the domains this server is responsible for.
|
|
type DomainRepository struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewDomainRepository(pool *pgxpool.Pool) *DomainRepository {
|
|
return &DomainRepository{pool: pool}
|
|
}
|
|
|
|
// ActiveDomainNames returns every active domain.
|
|
//
|
|
// The DNS reconciler used to run against MAIL_DOMAIN alone, so a domain added
|
|
// through the console was never reconciled: its records simply never appeared,
|
|
// and the only visible symptom was the console reporting them missing forever.
|
|
func (r *DomainRepository) ActiveDomainNames(ctx context.Context) ([]string, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT name FROM domains WHERE is_active ORDER BY name
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list active domains: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
out := []string{}
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, name)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// SaveDnsStatus files the outcome of a verification against the domain.
|
|
//
|
|
// The console reads its badge from this column, and nothing ever wrote to it
|
|
// after onboarding: a domain whose records were all published and resolving
|
|
// was still listed as PENDING, last checked "never".
|
|
func (r *DomainRepository) SaveDnsStatus(ctx context.Context, domain, status string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE domains
|
|
SET dns_status = $2, dns_last_checked_at = now()
|
|
WHERE lower(name) = lower($1)
|
|
`, domain, columnStatus(status))
|
|
if err != nil {
|
|
return fmt.Errorf("save dns status for %s: %w", domain, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// columnStatus maps the verifier's vocabulary onto the one the column accepts.
|
|
//
|
|
// dns_status is constrained to PENDING, VERIFIED, PARTIAL, DRIFT and ERROR,
|
|
// while verification also reports MISSING and UNKNOWN. Writing either of those
|
|
// violates the check constraint, so the update fails and the badge silently
|
|
// stays stale - the exact symptom this was meant to fix.
|
|
func columnStatus(status string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(status)) {
|
|
case "VERIFIED", "PARTIAL", "DRIFT", "ERROR", "PENDING":
|
|
return strings.ToUpper(strings.TrimSpace(status))
|
|
case "MISSING":
|
|
// Nothing resolved: the zone is not serving what it should.
|
|
return "ERROR"
|
|
default:
|
|
// UNKNOWN, and anything new the verifier learns to say.
|
|
return "PENDING"
|
|
}
|
|
}
|