fix(setup): CompleteAsNode idempotent — konvertiert Standalone zu Cluster-Node

Wenn setup bereits abgeschlossen war (z.B. zuvor als Standalone eingerichtet),
wird der Node jetzt ohne Fehler auf is_cluster_node=true umgestellt statt
mit "setup already completed" zu fehlschlagen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-29 17:21:17 +02:00
parent 2e84268f61
commit 6a5aff69d9
5 changed files with 18 additions and 8 deletions

View File

@@ -168,6 +168,10 @@ func (s *Store) Complete(req Request) (*State, error) {
// CompleteAsNode configures this node as a secondary cluster member.
// No admin credentials are created; login becomes available after PG
// replication brings users from the primary.
//
// Idempotent: if setup was already completed (e.g. the node was
// previously configured as standalone), it is converted to cluster-node
// mode. Calling again after a successful join is a no-op.
func (s *Store) CompleteAsNode(req NodeRequest) (*State, error) {
if !looksLikeFQDN(req.FQDN) {
return nil, fmt.Errorf("fqdn %q does not look like a fully-qualified hostname", req.FQDN)
@@ -179,16 +183,22 @@ func (s *Store) CompleteAsNode(req NodeRequest) (*State, error) {
if err != nil {
return nil, err
}
if prev.Completed {
return nil, errors.New("setup already completed")
}
now := time.Now().UTC()
completedAt := &now
if prev.CompletedAt != nil {
completedAt = prev.CompletedAt
}
st := &State{
FQDN: strings.TrimSpace(req.FQDN),
ACMEEmail: strings.ToLower(strings.TrimSpace(req.ACMEEmail)),
IsClusterNode: true,
Completed: true,
CompletedAt: &now,
CompletedAt: completedAt,
// Carry over non-auth fields from previous state.
LicenseKey: prev.LicenseKey,
IPv6Enabled: prev.IPv6Enabled,
BackupRetentionKeep: prev.BackupRetentionKeep,
AuditRetentionDays: prev.AuditRetentionDays,
}
if err := s.Save(st); err != nil {
return nil, err