fix(system): render-configs rendert jetzt ALLE Dienste, nicht nur HAProxy

- SystemHandler.ExtraReloaders: neues Feld für nftables, wireguard,
  squid, unbound, chrony Reloader
- WithAllReloaders(): Wire-Methode; main.go bindet alle Reloader nach
  Initialisierung ein
- RenderConfigs: läuft alle Reloader durch, sammelt Fehler, antwortet
  mit {ok, rendered[], errors{}} — partiell OK wird als warning gemeldet
- UI: Settings-Button-Label "Re-render all configs"; partial-error
  Toast zeigt welche Dienste fehlschlugen
- Timeout: 30s statt 10s (alle Dienste brauchen mehr Zeit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-24 22:29:00 +02:00
parent 8d7a43bc8c
commit 73619c17f8
8 changed files with 71 additions and 24 deletions

View File

@@ -1 +1 @@
1.1.95
1.1.96

View File

@@ -60,7 +60,7 @@ import (
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
)
var version = "1.1.95"
var version = "1.1.96"
func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR")
@@ -401,6 +401,16 @@ func main() {
}
handlers.NewNTPHandler(ntpRepo, auditRepo, nodeID, withFW(chronyReloader)).Register(authed)
// Wire all service reloaders into systemHdl so RenderConfigs
// re-renders every service from DB state in one shot.
systemHdl.WithAllReloaders(map[string]func(context.Context) error{
"nftables": fwReloader,
"wireguard": wgReloader,
"squid": squidReloader,
"unbound": unboundReloader,
"chrony": chronyReloader,
})
// License — node-local key store + DB-mirror of last verify
// result. Real verify runs against license.netcell-it.com via
// internal/license; the scheduler triggers daily re-verify.

View File

@@ -11,7 +11,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
)
var version = "1.1.95"
var version = "1.1.96"
const usage = `edgeguard-ctl — EdgeGuard CLI

View File

@@ -35,7 +35,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
)
var version = "1.1.95"
var version = "1.1.96"
const (
// renewTickInterval — how often we re-evaluate expiring certs.

View File

@@ -40,6 +40,9 @@ type SystemHandler struct {
// in main.go after generators are constructed. Read-only access —
// no writes, no reloads.
ConfigPreviewers map[string]func(stdcontext.Context) (string, error)
// ExtraReloaders: additional service renderers triggered by
// RenderConfigs. Keyed by service name (nftables, wireguard, etc.).
ExtraReloaders map[string]func(stdcontext.Context) error
}
func NewSystemHandler(version string) *SystemHandler {
@@ -75,6 +78,14 @@ func (h *SystemHandler) WithConfigPreviewers(previewers map[string]func(stdconte
return h
}
// WithAllReloaders injectet alle Service-Reloader für RenderConfigs.
// Reihenfolge: haproxy ist bereits in HAProxyReloader; extras sind die
// restlichen Dienste (nftables, wireguard, squid, unbound, chrony).
func (h *SystemHandler) WithAllReloaders(extras map[string]func(stdcontext.Context) error) *SystemHandler {
h.ExtraReloaders = extras
return h
}
func (h *SystemHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/system")
g.GET("/health", h.Health)
@@ -565,30 +576,46 @@ func (h *SystemHandler) ServiceRestart(c *gin.Context) {
}
// RenderConfigs erzwingt ein Re-Render aller Service-Configs aus dem
// aktuellen DB-State. Wenn der HAProxyReloader gesetzt ist, läuft der
// (rendert haproxy.cfg + reload). Praktisch wenn ein Operator denkt
// dass die generierte Config nicht mehr mit der DB übereinstimmt
// (Drift, Manual-Edit, etc.).
//
// v1 macht NUR haproxy — weitere Renderer (firewall, dns, ntp, wg,
// squid) sind per Handler an die jeweiligen Mutations-Endpoints
// gekoppelt; für die fehlt aktuell ein generisches "render all".
// aktuellen DB-State. Läuft haproxy + alle ExtraReloaders (nftables,
// wireguard, squid, unbound, chrony) durch. Fehler werden gesammelt
// und als partial-Antwort zurückgegeben — erfolgreich gerenderte
// Dienste stehen in "rendered", fehlgeschlagene in "errors".
func (h *SystemHandler) RenderConfigs(c *gin.Context) {
if h.HAProxyReloader == nil {
if h.HAProxyReloader == nil && len(h.ExtraReloaders) == 0 {
response.Err(c, http.StatusServiceUnavailable, simpleErr("renderer not wired"))
return
}
ctx, cancel := stdcontext.WithTimeout(c.Request.Context(), 10*time.Second)
ctx, cancel := stdcontext.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
if err := h.HAProxyReloader(ctx); err != nil {
response.Internal(c, err)
return
rendered := []string{}
errs := map[string]string{}
if h.HAProxyReloader != nil {
if err := h.HAProxyReloader(ctx); err != nil {
errs["haproxy"] = err.Error()
} else {
rendered = append(rendered, "haproxy")
}
}
// Defined order so the audit log is deterministic.
order := []string{"nftables", "wireguard", "squid", "unbound", "chrony"}
for _, name := range order {
fn, ok := h.ExtraReloaders[name]
if !ok {
continue
}
if err := fn(ctx); err != nil {
errs[name] = err.Error()
} else {
rendered = append(rendered, name)
}
}
if h.Audit != nil {
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "system.render_configs",
"", gin.H{}, h.NodeID)
"", gin.H{"rendered": rendered, "errors": errs}, h.NodeID)
}
response.OK(c, gin.H{"ok": true, "rendered": []string{"haproxy"}})
response.OK(c, gin.H{"ok": len(errs) == 0, "rendered": rendered, "errors": errs})
}
// UpgradeStatus liefert den Status des letzten Self-Upgrade-Versuchs.

View File

@@ -531,9 +531,10 @@
"haproxyReloadBtn": "HAProxy reload",
"haproxyReloadOk": "HAProxy neu geladen.",
"haproxyReloadFailed": "HAProxy-Reload fehlgeschlagen",
"renderConfigsBtn": "Configs neu rendern (HAProxy)",
"renderConfigsOk": "Configs neu gerendert + reloaded.",
"renderConfigsBtn": "Alle Configs neu rendern",
"renderConfigsOk": "Alle Configs neu gerendert und reloaded.",
"renderConfigsFailed": "Config-Render fehlgeschlagen",
"renderConfigsPartial": "Partieller Render — einige Dienste fehlgeschlagen: {{failed}}",
"backupNowBtn": "Backup jetzt erstellen",
"backupNowOk": "Backup ausgelöst — Status in der Backups-Seite verfolgen.",
"backupNowFailed": "Backup-Trigger fehlgeschlagen",

View File

@@ -531,9 +531,10 @@
"haproxyReloadBtn": "HAProxy reload",
"haproxyReloadOk": "HAProxy reloaded.",
"haproxyReloadFailed": "HAProxy reload failed",
"renderConfigsBtn": "Re-render configs (HAProxy)",
"renderConfigsOk": "Configs re-rendered + reloaded.",
"renderConfigsBtn": "Re-render all configs",
"renderConfigsOk": "All configs re-rendered and reloaded.",
"renderConfigsFailed": "Config render failed",
"renderConfigsPartial": "Partial render — some services failed: {{failed}}",
"backupNowBtn": "Backup now",
"serviceRestartCardTitle": "Restart services",
"serviceRestartBtn": "Restart",

View File

@@ -125,7 +125,15 @@ export default function SettingsPage() {
})
const renderConfigs = useMutation({
mutationFn: async () => apiClient.post('/system/render-configs'),
onSuccess: () => msg.success(t('settings.renderConfigsOk')),
onSuccess: (r) => {
const d = r.data?.data as { ok: boolean; rendered: string[]; errors: Record<string, string> } | undefined
if (d && !d.ok && d.errors && Object.keys(d.errors).length > 0) {
const failed = Object.entries(d.errors).map(([k, v]) => `${k}: ${v}`).join('; ')
msg.warning(t('settings.renderConfigsPartial', { failed }))
} else {
msg.success(t('settings.renderConfigsOk'))
}
},
onError: (e: Error) => msg.error(t('settings.renderConfigsFailed') + ': ' + e.message),
})
const triggerBackup = useMutation({