diff --git a/VERSION b/VERSION index 350e5c8..4743491 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.160 +1.1.161 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 7b18adb..eb11df1 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -60,7 +60,7 @@ import ( usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" ) -var version = "1.1.160" +var version = "1.1.161" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index d35a7b6..103a587 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -11,7 +11,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) -var version = "1.1.160" +var version = "1.1.161" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index ea0df2c..2fc406f 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -41,7 +41,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.160" +var version = "1.1.161" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/internal/handlers/cluster.go b/internal/handlers/cluster.go index 2b51c99..b5527c5 100644 --- a/internal/handlers/cluster.go +++ b/internal/handlers/cluster.go @@ -138,6 +138,27 @@ func (h *ClusterHandler) RegisterPublic(rg *gin.RouterGroup) { func (h *ClusterHandler) RegisterAgent(rg *gin.RouterGroup) { g := rg.Group("/agent/cluster") g.POST("/peers", h.AgentRegisterPeer) + g.GET("/identity", h.AgentIdentity) +} + +// AgentIdentity gibt die eigene ha_nodes-Row zurück. Wird vom Primary +// genutzt um joining-Peers aktiv zu reconcilen wenn autoRegister (Push) +// fehlgeschlagen ist — Pull-Fallback. +func (h *ClusterHandler) AgentIdentity(c *gin.Context) { + if h.Store == nil || h.LocalID == "" { + response.NotFound(c, simpleError("node not registered")) + return + } + node, err := h.Store.Get(c.Request.Context(), h.LocalID) + if err != nil { + if err == cluster.ErrNotFound { + response.NotFound(c, simpleError("local node not in ha_nodes yet")) + return + } + response.Internal(c, err) + return + } + response.OK(c, node) } // PeerReloader: optionale Funktion die nach einem Auto-Register @@ -212,6 +233,23 @@ func (h *ClusterHandler) Status(c *gin.Context) { if len(out.Peers) > 0 { out.Mode = "cluster" } + + // Pull-Reconcile für joining-Peers: wenn ein Peer via Aggregator + // erreichbar ist aber noch mit Placeholder-ID in ha_nodes steht, + // holen wir seine echte Identity aktiv ab. Best-effort goroutine — + // blockiert die Status-Response nicht. + if h.Aggregator != nil && h.Store != nil { + var joining []models.HANode + for _, p := range out.Peers { + if p.Status == "joining" || p.Status == "pending" { + joining = append(joining, p) + } + } + if len(joining) > 0 { + go h.reconcileJoiningPeers(joining) + } + } + // Drift-Detection: jeder peer mit anderem config_hash als unser // lokaler → Banner-Trigger im UI. if localHash != nil && *localHash != "" { @@ -458,6 +496,62 @@ func cnFromCSR(csrPEM string) string { return csr.Subject.CommonName } +// reconcileJoiningPeers versucht für jeden Peer im Status "joining" oder +// "pending" die echte Node-ID via /agent/cluster/identity zu holen und +// ihn in ha_nodes mit der richtigen ID einzutragen. Self-Healing- +// Fallback wenn autoRegister (Push von joining-Peer zu Primary) wegen +// eines temporären Netzwerkproblems fehlgeschlagen ist. +// +// Die public_ip des Placeholder-Rows wird in der neuen Row übernommen +// damit @peer_ipv4 (nftables) korrekt bleibt. +func (h *ClusterHandler) reconcileJoiningPeers(placeholders []models.HANode) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + results := h.Aggregator.FanOut(ctx, placeholders, "/agent/cluster/identity", h.LocalID) + changed := false + for i, res := range results { + if !res.OK || len(res.Data) == 0 { + continue + } + var identity models.HANode + if err := json.Unmarshal(res.Data, &identity); err != nil || identity.ID == "" { + continue + } + placeholder := placeholders[i] + if identity.ID == placeholder.ID { + continue // ID bereits korrekt + } + // Echte ID gefunden — row mit realer ID anlegen, public_ip aus + // dem Placeholder-Row übernehmen damit nftables korrekt bleibt. + n := identity + n.Status = "online" + if n.PublicIP == nil { + n.PublicIP = placeholder.PublicIP + } + if n.InternalIP == nil { + n.InternalIP = placeholder.InternalIP + } + out, err := h.Store.UpsertSelf(ctx, n) + if err != nil { + slog.Warn("cluster: reconcile joining peer: upsert failed", + "fqdn", n.FQDN, "real_id", n.ID, "error", err) + continue + } + _ = h.Store.DeletePlaceholdersByFQDN(ctx, n.FQDN, n.ID) + changed = true + slog.Info("cluster: joining peer reconciled via identity pull", + "id", out.ID, "fqdn", out.FQDN, "placeholder_id", placeholder.ID) + } + if changed && h.PeerReloader != nil { + rctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := h.PeerReloader(rctx); err != nil { + slog.Warn("cluster: PeerReloader failed after reconcile", "error", err) + } + } +} + var errInvalidJoinRequest = simpleError("missing token or csr") type simpleError string diff --git a/internal/services/clusterjoin/join.go b/internal/services/clusterjoin/join.go index 70bbdaa..a4380d2 100644 --- a/internal/services/clusterjoin/join.go +++ b/internal/services/clusterjoin/join.go @@ -126,14 +126,26 @@ func Join(req Request) error { // Auto-register: retry a few times because the primary's nftables may // need a moment to reload even though preRegisterJoiner is now // synchronous on the primary side. + var autoRegErr error for i := 0; i < 3; i++ { if err := autoRegister(primary, tlsDir, req.CommonName, req.Version, req.NodeID); err == nil { + autoRegErr = nil break - } else if i < 2 { - slog.Warn("clusterjoin: autoRegister failed, retrying", "attempt", i+1, "error", err) - time.Sleep(2 * time.Second) + } else { + autoRegErr = err + if i < 2 { + slog.Warn("clusterjoin: autoRegister failed, retrying", "attempt", i+1, "error", err) + time.Sleep(2 * time.Second) + } } } + if autoRegErr != nil { + slog.Warn("clusterjoin: autoRegister failed after all retries — primary will reconcile via identity pull", + "error", autoRegErr) + } + // Certs are saved; primary will reconcile via /agent/cluster/identity pull + // even if autoRegister failed. Not returning the error — join is + // structurally complete (certs issued), only the ha_nodes update is pending. return nil }