From 164640b932a96166b69f1a33746bf0a035cd1900 Mon Sep 17 00:00:00 2001 From: Kimmo Lehto Date: Thu, 8 Jan 2026 16:30:11 +0200 Subject: [PATCH 1/2] Allow controller role mismatch (single, controller, controller+worker) when resetting nodes Signed-off-by: Kimmo Lehto --- phase/gather_k0s_facts.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/phase/gather_k0s_facts.go b/phase/gather_k0s_facts.go index 6ec12f0b..36ad39d0 100644 --- a/phase/gather_k0s_facts.go +++ b/phase/gather_k0s_facts.go @@ -342,7 +342,12 @@ func (p *GatherK0sFacts) investigateK0s(ctx context.Context, h *cluster.Host) er } if status.Role != h.Role { - return fmt.Errorf("%s: is configured as k0s %s but is already running as %s - role change is not supported", h, h.Role, status.Role) + if !h.Reset || h.Role == "worker" || status.Role == "worker" { + return fmt.Errorf("%s: is configured as k0s %s but is already running as %s - role change is not supported", h, h.Role, status.Role) + } + + log.Warnf("%s: was configured as %s but is already running as %s - proceeding with reset using the discovered role", h, h.Role, status.Role) + h.Role = status.Role } h.Metadata.K0sRunningVersion = status.Version From 8d8c606bc4a8c899841d73ab08fad61016eead7f Mon Sep 17 00:00:00 2001 From: Kimmo Lehto Date: Thu, 8 Jan 2026 16:58:46 +0200 Subject: [PATCH 2/2] Allow any mismatch with --force Signed-off-by: Kimmo Lehto --- phase/gather_k0s_facts.go | 21 ++++++++++++++++----- phase/gather_k0s_facts_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/phase/gather_k0s_facts.go b/phase/gather_k0s_facts.go index 36ad39d0..3ab9a60e 100644 --- a/phase/gather_k0s_facts.go +++ b/phase/gather_k0s_facts.go @@ -342,12 +342,9 @@ func (p *GatherK0sFacts) investigateK0s(ctx context.Context, h *cluster.Host) er } if status.Role != h.Role { - if !h.Reset || h.Role == "worker" || status.Role == "worker" { - return fmt.Errorf("%s: is configured as k0s %s but is already running as %s - role change is not supported", h, h.Role, status.Role) + if err := p.handleRoleMismatch(h, status.Role); err != nil { + return err } - - log.Warnf("%s: was configured as %s but is already running as %s - proceeding with reset using the discovered role", h, h.Role, status.Role) - h.Role = status.Role } h.Metadata.K0sRunningVersion = status.Version @@ -410,6 +407,20 @@ func (p *GatherK0sFacts) investigateK0s(ctx context.Context, h *cluster.Host) er return nil } +func (p *GatherK0sFacts) handleRoleMismatch(h *cluster.Host, detectedRole string) error { + if !h.Reset { + return fmt.Errorf("%s: is configured as k0s %s but is already running as %s - role change is not supported", h, h.Role, detectedRole) + } + + if !Force { + return fmt.Errorf("%s: is configured as k0s %s but is already running as %s - role change is not supported, use --force to ignore the mismatch during reset", h, h.Role, detectedRole) + } + + log.Warnf("%s: was configured as %s but is already running as %s - proceeding with reset using the discovered role because --force was given", h, h.Role, detectedRole) + h.Role = detectedRole + return nil +} + func (p *GatherK0sFacts) needsUpgrade(h *cluster.Host) bool { if h.Reset { return false diff --git a/phase/gather_k0s_facts_test.go b/phase/gather_k0s_facts_test.go index ce7caed3..ceccf6e4 100644 --- a/phase/gather_k0s_facts_test.go +++ b/phase/gather_k0s_facts_test.go @@ -70,3 +70,33 @@ func TestReportUseExistingHostsFailsWithoutBinary(t *testing.T) { p.SetManager(mgr) require.ErrorContains(t, p.reportUseExistingHosts(), "useExistingK0s=true but no k0s binary found on host") } + +func TestHandleRoleMismatch(t *testing.T) { + originalForce := Force + t.Cleanup(func() { Force = originalForce }) + + p := GatherK0sFacts{} + + t.Run("host not marked for reset", func(t *testing.T) { + Force = true + h := &cluster.Host{Role: "controller"} + err := p.handleRoleMismatch(h, "worker") + require.ErrorContains(t, err, "role change is not supported") + require.Equal(t, "controller", h.Role) + }) + + t.Run("reset host requires force", func(t *testing.T) { + Force = false + h := &cluster.Host{Role: "controller", Reset: true} + err := p.handleRoleMismatch(h, "single") + require.ErrorContains(t, err, "use --force") + require.Equal(t, "controller", h.Role) + }) + + t.Run("force allows role update", func(t *testing.T) { + Force = true + h := &cluster.Host{Role: "controller", Reset: true} + require.NoError(t, p.handleRoleMismatch(h, "worker")) + require.Equal(t, "worker", h.Role) + }) +}