fix(dns): o botao de reconciliar nao criava registo nenhum
O painel listava os registos em falta e oferecia um botao para os criar, mas esse botao chegava a um endpoint que so escrevia uma linha de auditoria e respondia "REQUESTED". Nunca tocou no DNS. Quem tinha o token da Cloudflare era o servico de protocolos, e a unica coisa capaz de o usar corria num temporizador -- para um dominio so, o de MAIL_DOMAIN. Por isso um dominio adicionado pela consola, como o cienciaembarcada.com.br, nunca era reconciliado: os registos nao apareciam e o unico sintoma visivel era a consola dizer que faltavam, para sempre. - POST /api/v1/admin/dns/reconcile publica os registos de um dominio, agora, e devolve o que foi criado, corrigido e o que ficou em conflito - o temporizador passa a percorrer todos os dominios ativos - conflitos sao reportados, nao resolvidos: um registo do mesmo tipo e nome com outro valor pode ser correio da zona ou um servico de que ninguem se lembra, e nao e deste servidor decidir sobrepo-lo - o erro do fornecedor fica no log, nao na resposta: pode nomear a conta ou a zona Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8227fe2b97
commit
2b369ab373
11 files changed
+497
-17
No files matched your search
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
"lambdamail/protocols/internal/application/usecase"
|
||||
"lambdamail/protocols/internal/infrastructure/cloudflare"
|
||||
"lambdamail/protocols/internal/infrastructure/netdns"
|
||||
"lambdamail/protocols/internal/infrastructure/postgres"
|
||||
httppresentation "lambdamail/protocols/internal/presentation/http"
|
||||
)
|
||||
|
||||
// domainReconciler publishes one domain's expected records on demand.
|
||||
//
|
||||
// The console could tell an operator a record was missing and offer no way to
|
||||
// create it: the button reached an endpoint that wrote an audit row, and the
|
||||
// only code able to talk to the DNS provider ran on a timer, for one domain.
|
||||
type domainReconciler struct {
|
||||
run func(ctx context.Context, domain string) (*usecase.SyncDnsRecordsOutput, error)
|
||||
}
|
||||
|
||||
func (d domainReconciler) ReconcileDomain(
|
||||
ctx context.Context, domain string,
|
||||
) (httppresentation.ReconcileResult, error) {
|
||||
out, err := d.run(ctx, domain)
|
||||
if err != nil {
|
||||
return httppresentation.ReconcileResult{}, err
|
||||
}
|
||||
|
||||
conflicts := make([]string, 0, len(out.Conflicts))
|
||||
for _, c := range out.Conflicts {
|
||||
// Named plainly, because the operator has to decide what to do: the
|
||||
// existing value may be something they still need.
|
||||
conflicts = append(conflicts, fmt.Sprintf("%s %s already holds %q", c.Type, c.Name, c.Existing))
|
||||
}
|
||||
|
||||
return httppresentation.ReconcileResult{
|
||||
Domain: domain,
|
||||
Created: out.CreatedCount,
|
||||
Updated: out.UpdatedCount,
|
||||
Unchanged: out.UnchangedCount,
|
||||
Conflicts: conflicts,
|
||||
Errors: out.Errors,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// reconcileDomainOnDemand publishes one domain's records, now.
|
||||
//
|
||||
// It builds the same use case the timer uses rather than sharing an instance:
|
||||
// the console's button and the background sweep must not be able to interleave
|
||||
// halfway through each other's zone updates.
|
||||
func reconcileDomainOnDemand(
|
||||
ctx context.Context, cfg config, aliasRepo port.SystemAliasRepository,
|
||||
dkimRepo *postgres.DkimRepository, domain string,
|
||||
) (*usecase.SyncDnsRecordsOutput, error) {
|
||||
syncUC := usecase.NewSyncDnsRecordsUseCase(cloudflare.NewCloudflareAdapter(cfg.CloudflareToken), aliasRepo)
|
||||
syncUC.SetVerifier(netdns.NewPublicVerifier())
|
||||
|
||||
input := usecase.SyncDnsRecordsInput{
|
||||
DomainName: domain,
|
||||
MailHost: cfg.PrimaryMailHost,
|
||||
ServerIPv4: cfg.PublicIPv4,
|
||||
ServerIPv6: cfg.PublicIPv6,
|
||||
DaneEnabled: cfg.OutboundDane,
|
||||
RelaySpfInclude: relayConfig(cfg).SpfInclude(),
|
||||
}
|
||||
|
||||
// A domain with no DKIM key of its own would publish a selector pointing
|
||||
// at nothing, so the keys are provisioned first, exactly as the timer does.
|
||||
if dkimRepo != nil {
|
||||
keys, err := usecase.NewProvisionDkimKeysUseCase(dkimRepo, generateDkimKey).Execute(ctx, domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("provision DKIM keys for %s: %w", domain, err)
|
||||
}
|
||||
input.RsaDkimPubKey = keys.RsaPublicKey
|
||||
input.EdDkimPubKey = keys.Ed25519PublicKey
|
||||
if len(keys.Created) > 0 {
|
||||
log.Printf("DNS reconcile (on demand): generated DKIM keys for %s: %v", domain, keys.Created)
|
||||
}
|
||||
}
|
||||
|
||||
out, err := syncUC.Execute(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("DNS reconcile (on demand) for %s: %s (created=%d updated=%d unchanged=%d conflicts=%d)",
|
||||
domain, out.Status, out.CreatedCount, out.UpdatedCount, out.UnchangedCount, out.ConflictCount)
|
||||
return out, nil
|
||||
}
|
||||
Reference in new issue
Block a user