feat(breakglass): add halt-a-server op to the recovery console (§B4)

Add a root-gated "Halt a running server" operation to the break-glass
console. The operator picks a server from the live fleet and the console
flips that MinecraftServer CRD's spec.desiredState to Stopped via a
spec-only merge patch, disjoint from the operator's status writes, so it
cannot race or clobber reconciliation. It is the panel-independent
emergency stop for when the box still has root plus a kubeconfig.

System servers (login/lobby) are allowed but flagged: a system tag in the
picker and an explicit WARNING in the post-exit summary, since halting
login takes the shared auth front door down with no fallback. Audit is
best-effort so a halt still works with the audit sink down. Already-stopped
is a distinct no-op. The core (halt.go) is unit-tested against a real
controller-runtime fake client that applies the patch.
This commit is contained in:
flyemoji committed 2026-07-06 23:50:09 +09:00
1 parent abad137a01
commit c2ee21ae05
8 files changed
+649 -10

No files matched your search

+26 -1
View File
@@ -139,10 +139,11 @@ type rootModel struct {
adminHost string
panelHost string
accessAud string
namespace string // minecraft workload namespace (cfg.K8s.Namespace); target of the halt op
adminExists bool
}
func newRootModel(ctx context.Context, store ownerStore, dbURL, rootDomain, adminHostname, panelHostname, accessAud, osUser string, adminExists bool, mode consoleMode) *rootModel {
func newRootModel(ctx context.Context, store ownerStore, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool, mode consoleMode) *rootModel {
rm := &rootModel{
ctx: ctx,
reviewing: -1,
@@ -153,6 +154,7 @@ func newRootModel(ctx context.Context, store ownerStore, dbURL, rootDomain, admi
adminHost: adminHostname,
panelHost: panelHostname,
accessAud: accessAud,
namespace: namespace,
adminExists: adminExists,
mode: mode,
result: breakGlassResult{
@@ -219,10 +221,33 @@ func (m *rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.op {
case bgAddOperator:
return m.adopt(newOperatorModel(m.ctx, m.store, m.osUser))
case bgHaltServer:
return m.adopt(newHaltModel(m.ctx, m.store, m.namespace, m.osUser))
default:
return m.adopt(newOwnerModel(m.ctx, m.store, m.osUser, m.adminExists))
}
case haltResultMsg:
// Halt is terminal in break-glass: record the durable summary (so cmdBreakGlass
// can re-print it past the alt-screen teardown) and quit. A load/perform failure
// routes through the root's error path; an empty fleet or cancel leaves halted
// false, so the summary reports "no changes made".
if msg.err != nil {
m.err = msg.err
return m, tea.Quit
}
if msg.done {
m.result.halted = true
m.result.haltServer = msg.outcome.name
m.result.haltNamespace = msg.outcome.namespace
m.result.haltAlreadyStopped = msg.outcome.alreadyStopped
m.result.haltSystemServer = msg.outcome.system
if msg.outcome.auditErr != nil {
m.result.haltAuditWarning = msg.outcome.auditErr.Error()
}
}
return m, tea.Quit
case ownerResultMsg:
if msg.err != nil {
m.err = msg.err