diff --git a/VERSION b/VERSION index 2ac243d..a0090b5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.155 +1.1.156 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 7ab7f8b..6063320 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.155" +var version = "1.1.156" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") @@ -279,6 +279,9 @@ func main() { setupHdl.WithClusterSupport(clusterStore, func(ctx context.Context) error { return firewallrender.New(pool).Render(ctx) }) + // Cluster-Node-Startup: Primary in lokalen ha_nodes eintragen damit + // nftables @peer_ipv4 korrekt ist — auch ohne erneuten Join. + go setupHdl.StartupPeerSync() usersRepo := usersvc.New(pool) authHdl.WithAudit(auditRepo, nodeID).WithUsers(usersRepo) diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index 870f8df..8c7c002 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.155" +var version = "1.1.156" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index 93a9df7..54a9664 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.155" +var version = "1.1.156" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index ff29d04..975cc62 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -191,8 +191,9 @@ func (h *SetupHandler) JoinCluster(c *gin.Context) { } st, err := h.Store.CompleteAsNode(setup.NodeRequest{ - FQDN: fqdn, - ACMEEmail: body.ACMEEmail, + FQDN: fqdn, + ACMEEmail: body.ACMEEmail, + PrimaryFQDN: strings.ToLower(strings.TrimSpace(body.PrimaryFQDN)), }) if err != nil { response.Internal(c, err) @@ -212,8 +213,24 @@ func (h *SetupHandler) JoinCluster(c *gin.Context) { }) } +// StartupPeerSync is called once after the DB pool and ClusterStore are +// ready. On cluster nodes it re-registers the primary in the local ha_nodes +// and reloads nftables so @peer_ipv4 is correct after a package update or +// reboot — without requiring a new join. +func (h *SetupHandler) StartupPeerSync() { + if h.ClusterStore == nil || h.PeerReloader == nil { + return + } + st, err := h.Store.Load() + if err != nil || st == nil || !st.IsClusterNode || st.PrimaryFQDN == "" { + return + } + h.preRegisterPrimary(st.PrimaryFQDN) +} + // preRegisterPrimary inserts the primary node into the local ha_nodes with // its resolved IP so nftables @peer_ipv4 allows port 8443 from the primary. +// Uses a stable ID derived from the FQDN so repeated calls are idempotent. func (h *SetupHandler) preRegisterPrimary(primaryFQDN string) { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() @@ -228,7 +245,8 @@ func (h *SetupHandler) preRegisterPrimary(primaryFQDN string) { } ip := addrs[0] - nodeID := fmt.Sprintf("primary-%x", time.Now().UnixNano()) + // Stable ID so repeated calls (join + startup) don't accumulate rows. + nodeID := fmt.Sprintf("prenode-%s", strings.ReplaceAll(primaryFQDN, ".", "-")) n := models.HANode{ ID: nodeID, Name: primaryFQDN, diff --git a/internal/services/setup/setup.go b/internal/services/setup/setup.go index 5890abb..f129180 100644 --- a/internal/services/setup/setup.go +++ b/internal/services/setup/setup.go @@ -65,6 +65,12 @@ type State struct { // [::]:443 und [::]:3443. Default false weil nicht alle Deployments // IPv6 haben. Nach Änderung wird HAProxy neu geladen. IPv6Enabled bool `json:"ipv6_enabled,omitempty"` + + // PrimaryFQDN: FQDN des Cluster-Primary, den dieser Node beim Join + // angegeben hat. Wird bei jedem API-Start genutzt um den Primary in + // der lokalen ha_nodes vorab zu registrieren (Firewall @peer_ipv4), + // ohne dass der Join erneut durchlaufen werden muss. + PrimaryFQDN string `json:"primary_fqdn,omitempty"` } // Request is the JSON body POST /api/v1/setup/complete accepts. @@ -81,8 +87,9 @@ type Request struct { // NodeRequest is the JSON body POST /api/v1/setup/complete-node accepts. // No admin credentials — they are replicated from the primary via PG. type NodeRequest struct { - FQDN string `json:"fqdn" binding:"required"` - ACMEEmail string `json:"acme_email" binding:"required,email"` + FQDN string `json:"fqdn" binding:"required"` + ACMEEmail string `json:"acme_email" binding:"required,email"` + PrimaryFQDN string `json:"primary_fqdn,omitempty"` } type Store struct { @@ -188,12 +195,17 @@ func (s *Store) CompleteAsNode(req NodeRequest) (*State, error) { if prev.CompletedAt != nil { completedAt = prev.CompletedAt } + primaryFQDN := strings.ToLower(strings.TrimSpace(req.PrimaryFQDN)) + if primaryFQDN == "" { + primaryFQDN = prev.PrimaryFQDN // carry over if not re-supplied + } st := &State{ FQDN: strings.TrimSpace(req.FQDN), ACMEEmail: strings.ToLower(strings.TrimSpace(req.ACMEEmail)), IsClusterNode: true, Completed: true, CompletedAt: completedAt, + PrimaryFQDN: primaryFQDN, // Carry over non-auth fields from previous state. LicenseKey: prev.LicenseKey, IPv6Enabled: prev.IPv6Enabled,