feat: Wire up service registration to networking reconciliation
When a service is registered, updated, or deregistered, Central now automatically regenerates HAProxy routes and dnsmasq DNS entries to match the current set of registered services. - L4 tcp-passthrough services (Wild Cloud) → HAProxy SNI routes - L7 http services (Wild Works) → HAProxy Host-based reverse proxy - All services with reach != off → dnsmasq DNS entries - SSE events broadcast on config changes - Reconciliation runs asynchronously (non-blocking) This closes the loop: service registration is no longer inert — it actually updates the networking stack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,10 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
|
|||||||
sseManager: sseManager,
|
sseManager: sseManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wire up service registration reconciliation: when services change,
|
||||||
|
// regenerate dnsmasq DNS entries and HAProxy routes.
|
||||||
|
api.services.SetReconcileFn(api.reconcileNetworking)
|
||||||
|
|
||||||
return api, nil
|
return api, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,118 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wild-cloud/wild-central/internal/config"
|
||||||
|
"github.com/wild-cloud/wild-central/internal/haproxy"
|
||||||
|
"github.com/wild-cloud/wild-central/internal/services"
|
||||||
"github.com/wild-cloud/wild-central/internal/sse"
|
"github.com/wild-cloud/wild-central/internal/sse"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// reconcileNetworking reads all registered services and regenerates
|
||||||
|
// dnsmasq DNS entries and HAProxy routes to match. Called automatically
|
||||||
|
// whenever a service is registered, updated, or deregistered.
|
||||||
|
func (api *API) reconcileNetworking() {
|
||||||
|
svcs, err := api.services.List()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("reconcile: failed to list services", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
globalConfigPath := filepath.Join(api.dataDir, "config.yaml")
|
||||||
|
globalCfg, err := config.LoadGlobalConfig(globalConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("reconcile: failed to load global config, using empty", "error", err)
|
||||||
|
globalCfg = &config.GlobalConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HAProxy routes from registered services
|
||||||
|
var instanceRoutes []haproxy.InstanceRoute
|
||||||
|
var httpRoutes []haproxy.HTTPRoute
|
||||||
|
|
||||||
|
for _, svc := range svcs {
|
||||||
|
if svc.Reach == services.ReachOff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch svc.Backend.Type {
|
||||||
|
case services.BackendTCPPassthrough:
|
||||||
|
instanceRoutes = append(instanceRoutes, haproxy.InstanceRoute{
|
||||||
|
Name: svc.Name,
|
||||||
|
Domain: svc.Domain,
|
||||||
|
BackendIP: extractHost(svc.Backend.Address),
|
||||||
|
ExtraDomains: svc.ExtraDomains,
|
||||||
|
})
|
||||||
|
case services.BackendHTTP, services.BackendStatic:
|
||||||
|
httpRoutes = append(httpRoutes, haproxy.HTTPRoute{
|
||||||
|
Name: svc.Name,
|
||||||
|
Domain: svc.Domain,
|
||||||
|
Backend: svc.Backend.Address,
|
||||||
|
HealthPath: svc.Backend.Health,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine central domain for HAProxy
|
||||||
|
centralDomain := ""
|
||||||
|
if globalCfg.Cloud.Central.Domain != "" {
|
||||||
|
centralDomain = globalCfg.Cloud.Central.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and write HAProxy config
|
||||||
|
haproxyCfg := api.haproxy.GenerateWithOpts(instanceRoutes, nil, haproxy.GenerateOpts{
|
||||||
|
CentralDomain: centralDomain,
|
||||||
|
HTTPRoutes: httpRoutes,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := api.haproxy.WriteConfig(haproxyCfg); err != nil {
|
||||||
|
slog.Error("reconcile: failed to write HAProxy config", "error", err)
|
||||||
|
} else {
|
||||||
|
if err := api.haproxy.ReloadService(); err != nil {
|
||||||
|
slog.Warn("reconcile: failed to reload HAProxy", "error", err)
|
||||||
|
} else {
|
||||||
|
api.broadcastHaproxyEvent("haproxy:config", "HAProxy config regenerated from registered services")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate dnsmasq DNS entries from registered services
|
||||||
|
// Build instance configs for backward compatibility with dnsmasq generator
|
||||||
|
var instanceConfigs []config.InstanceConfig
|
||||||
|
for _, svc := range svcs {
|
||||||
|
if svc.Reach == services.ReachOff || svc.Domain == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ic := config.InstanceConfig{}
|
||||||
|
ic.Cloud.Domain = svc.Domain
|
||||||
|
ic.Cluster.LoadBalancerIp = extractHost(svc.Backend.Address)
|
||||||
|
instanceConfigs = append(instanceConfigs, ic)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.dnsmasq.UpdateConfig(globalCfg, instanceConfigs, true); err != nil {
|
||||||
|
slog.Error("reconcile: failed to update dnsmasq", "error", err)
|
||||||
|
} else {
|
||||||
|
api.broadcastDnsmasqEvent("dnsmasq:config", "DNS config regenerated from registered services")
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("reconcile: networking updated",
|
||||||
|
"services", len(svcs),
|
||||||
|
"l4Routes", len(instanceRoutes),
|
||||||
|
"l7Routes", len(httpRoutes),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractHost gets the host part from a host:port string
|
||||||
|
func extractHost(addr string) string {
|
||||||
|
for i := len(addr) - 1; i >= 0; i-- {
|
||||||
|
if addr[i] == ':' {
|
||||||
|
return addr[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
// broadcastDnsmasqEvent broadcasts SSE events for dnsmasq status changes
|
// broadcastDnsmasqEvent broadcasts SSE events for dnsmasq status changes
|
||||||
func (api *API) broadcastDnsmasqEvent(eventType string, message string) {
|
func (api *API) broadcastDnsmasqEvent(eventType string, message string) {
|
||||||
if api.sseManager == nil {
|
if api.sseManager == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user