feat(cluster): startup peer-sync + stable primary-ID in ha_nodes
On API restart, cluster nodes now re-register their primary in the local ha_nodes and reload nftables so @peer_ipv4 is correct after a package update or reboot without requiring a re-join. Also fixes duplicate ha_nodes rows: preRegisterPrimary previously used time.Now().UnixNano() as node ID, creating a fresh row each call. Now uses a deterministic ID derived from the FQDN so repeated upserts are idempotent. PrimaryFQDN is now persisted in setup.json during CompleteAsNode so the startup sync knows which primary to contact. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user