feat(setup): Cluster-Join komplett in GUI — kein CLI mehr nötig

POST /setup/join-cluster macht alles was bisher edgeguard-ctl cluster-join
tat: CSR generieren, Certs vom Primary holen, schreiben, auto-registrieren,
Setup als Cluster-Node markieren.

Setup-Wizard Node-Modus fragt jetzt direkt Primary-FQDN + Join-Token ab.
Nach Submit: Erfolgsmeldung + einziger verbleibender Schritt (systemctl restart).

Neue interne Bibliothek: internal/services/clusterjoin — wird von Handler
und CLI (edgeguard-ctl cluster-join) gleichermaßen genutzt, keine Duplizierung.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-29 17:08:49 +02:00
parent 7e5a7a83c0
commit afe2c951b9
10 changed files with 437 additions and 321 deletions

View File

@@ -1,10 +1,13 @@
package handlers
import (
"strings"
"github.com/gin-gonic/gin"
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/clusterjoin"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
)
@@ -12,9 +15,10 @@ import (
// are mounted before SetupGate so they remain reachable while the API
// is in setup mode.
type SetupHandler struct {
Store *setup.Store
Audit *audit.Repo
NodeID string
Store *setup.Store
Audit *audit.Repo
NodeID string
Version string
}
func NewSetupHandler(store *setup.Store) *SetupHandler {
@@ -30,11 +34,18 @@ func (h *SetupHandler) WithAudit(a *audit.Repo, nodeID string) *SetupHandler {
return h
}
// WithVersion macht die laufende Version für auto-register verfügbar.
func (h *SetupHandler) WithVersion(v string) *SetupHandler {
h.Version = v
return h
}
func (h *SetupHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/setup")
g.GET("/status", h.Status)
g.POST("/complete", h.Complete)
g.POST("/complete-node", h.CompleteAsNode)
g.POST("/join-cluster", h.JoinCluster)
}
// RegisterAuthed mountet die Endpoints die nach abgeschlossenem Setup
@@ -130,3 +141,48 @@ func (h *SetupHandler) CompleteAsNode(c *gin.Context) {
"fqdn": st.FQDN,
})
}
// JoinCluster performs the full cluster-join flow from the setup wizard:
// fetches certs from the primary, writes them to disk, then marks setup
// as completed. No CLI required.
func (h *SetupHandler) JoinCluster(c *gin.Context) {
var body struct {
FQDN string `json:"fqdn" binding:"required"`
ACMEEmail string `json:"acme_email" binding:"required,email"`
PrimaryFQDN string `json:"primary_fqdn" binding:"required"`
Token string `json:"token" binding:"required"`
Insecure bool `json:"insecure"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, err)
return
}
fqdn := strings.ToLower(strings.TrimSpace(body.FQDN))
if err := clusterjoin.Join(clusterjoin.Request{
PrimaryFQDN: body.PrimaryFQDN,
Token: strings.TrimSpace(body.Token),
CommonName: fqdn,
Insecure: body.Insecure,
Version: h.Version,
NodeID: h.NodeID,
}); err != nil {
response.BadRequest(c, err)
return
}
st, err := h.Store.CompleteAsNode(setup.NodeRequest{
FQDN: fqdn,
ACMEEmail: body.ACMEEmail,
})
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{
"completed": st.Completed,
"is_cluster_node": st.IsClusterNode,
"fqdn": st.FQDN,
})
}