feat: Cloudflare DNS management with automatic record sync

Add flexible DNS management to Docker Watcher. By default, wildcard DNS
is assumed (current behavior). When disabled, users can configure a
Cloudflare DNS provider with API token and zone selection. DNS A records
are automatically created/updated/deleted in sync with proxy consumers
(deployed instances and standalone proxies).

- Settings: wildcard_dns toggle, dns_provider, cloudflare credentials
- Cloudflare client: Provider interface with EnsureRecord/DeleteRecord/ListRecords
- DNS lifecycle hooks in deployer and proxy manager (best-effort)
- Settings UI: DNS config section with provider picker, zone selector, test button
- DNS Records page at /dns with filtering, sync status, reconciliation
- Records visible in both wildcard and managed modes
- Cleanup on provider change: removes old records when switching modes
This commit is contained in:
2026-04-02 14:49:21 +03:00
parent c9d4895ee3
commit c730cfaa45
46 changed files with 2429 additions and 1260 deletions
+25
View File
@@ -8,6 +8,7 @@ import (
"github.com/alexei/docker-watcher/internal/auth"
"github.com/alexei/docker-watcher/internal/crypto"
"github.com/alexei/docker-watcher/internal/dns"
"github.com/alexei/docker-watcher/internal/docker"
"github.com/alexei/docker-watcher/internal/events"
"github.com/alexei/docker-watcher/internal/npm"
@@ -17,6 +18,10 @@ import (
"github.com/alexei/docker-watcher/internal/webhook"
)
// DNSProviderChangedFunc is called when DNS settings change so the caller can
// update the provider on the deployer and proxy manager.
type DNSProviderChangedFunc func(provider dns.Provider)
// Server holds all dependencies for the API layer.
type Server struct {
store *store.Store
@@ -30,6 +35,9 @@ type Server struct {
oidcProvider *auth.OIDCProvider
staleScanner *stale.Scanner
proxyManager *proxy.Manager
dnsProvider dns.Provider
onDNSProviderChanged DNSProviderChangedFunc
}
// NewServer creates a new API Server with all required dependencies.
@@ -76,6 +84,16 @@ func (s *Server) SetProxyManager(pm *proxy.Manager) {
s.proxyManager = pm
}
// SetDNSProvider sets the current DNS provider on the server.
func (s *Server) SetDNSProvider(provider dns.Provider) {
s.dnsProvider = provider
}
// SetDNSProviderChangedCallback sets the callback for when DNS settings change.
func (s *Server) SetDNSProviderChangedCallback(fn DNSProviderChangedFunc) {
s.onDNSProviderChanged = fn
}
// initOIDCProvider creates an OIDC provider from settings. Errors are logged, not fatal.
func (s *Server) initOIDCProvider(ctx context.Context, as store.AuthSettings) {
// Decrypt the OIDC client secret if it's encrypted.
@@ -251,6 +269,13 @@ func (s *Server) Router() chi.Router {
r.Put("/settings", s.updateSettings)
r.Get("/settings/webhook-url", s.getWebhookURL)
r.Post("/settings/webhook-url/regenerate", s.regenerateWebhookSecret)
// DNS management endpoints.
r.Post("/settings/dns/test", s.testDNSConnection)
r.Get("/settings/dns/zones", s.listDNSZones)
r.Get("/dns/records", s.listDNSRecords)
r.Post("/dns/sync", s.syncDNSRecords)
r.Delete("/dns/records/{fqdn}", s.deleteDNSRecord)
})
})
})