Files
Felis/cmd/felis/halt_test.go
T
flyemoji c2ee21ae05 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.
2026-07-06 23:50:09 +09:00

180 lines
6.0 KiB
Go

package main
import (
"context"
"strings"
"testing"
"felis.lolicon.best/internal/apis/felis/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const haltNS = "minecraft"
func haltScheme(t *testing.T) *runtime.Scheme {
t.Helper()
scheme := runtime.NewScheme()
if err := clientgoscheme.AddToScheme(scheme); err != nil {
t.Fatalf("clientgo scheme: %v", err)
}
if err := v1alpha1.AddToScheme(scheme); err != nil {
t.Fatalf("v1alpha1 scheme: %v", err)
}
return scheme
}
func mcServer(name string, desired v1alpha1.DesiredState, phase v1alpha1.Phase) *v1alpha1.MinecraftServer {
return &v1alpha1.MinecraftServer{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: haltNS},
Spec: v1alpha1.MinecraftServerSpec{DesiredState: desired},
Status: v1alpha1.MinecraftServerStatus{Phase: phase},
}
}
func haltClient(t *testing.T, objs ...client.Object) client.Client {
t.Helper()
return fake.NewClientBuilder().WithScheme(haltScheme(t)).WithObjects(objs...).Build()
}
// desiredStateOf reads a server's spec.desiredState straight back from the client,
// so the patch is verified by its actual persisted effect, not by "Patch was called".
func desiredStateOf(t *testing.T, cl client.Client, name string) v1alpha1.DesiredState {
t.Helper()
var ms v1alpha1.MinecraftServer
if err := cl.Get(context.Background(), types.NamespacedName{Namespace: haltNS, Name: name}, &ms); err != nil {
t.Fatalf("get %q back: %v", name, err)
}
return ms.Spec.DesiredState
}
func TestHaltServer(t *testing.T) {
ctx := context.Background()
t.Run("running server is patched to Stopped", func(t *testing.T) {
cl := haltClient(t, mcServer("survival", v1alpha1.DesiredRunning, v1alpha1.PhaseRunning))
out, err := haltServer(ctx, cl, haltNS, "survival")
if err != nil {
t.Fatalf("haltServer: %v", err)
}
if out.alreadyStopped {
t.Error("alreadyStopped = true, want false for a running server")
}
if got := desiredStateOf(t, cl, "survival"); got != v1alpha1.DesiredStopped {
t.Errorf("desiredState after halt = %q, want Stopped", got)
}
})
t.Run("already-stopped server is a reported no-op", func(t *testing.T) {
cl := haltClient(t, mcServer("survival", v1alpha1.DesiredStopped, v1alpha1.PhaseStopped))
out, err := haltServer(ctx, cl, haltNS, "survival")
if err != nil {
t.Fatalf("haltServer: %v", err)
}
if !out.alreadyStopped {
t.Error("alreadyStopped = false, want true for an already-stopped server")
}
if got := desiredStateOf(t, cl, "survival"); got != v1alpha1.DesiredStopped {
t.Errorf("desiredState = %q, want it left Stopped", got)
}
})
t.Run("missing server is a clear error, not a patch", func(t *testing.T) {
cl := haltClient(t)
_, err := haltServer(ctx, cl, haltNS, "ghost")
if err == nil {
t.Fatal("haltServer(ghost) = nil error, want not-found error")
}
if !strings.Contains(err.Error(), "ghost") {
t.Errorf("error %q does not name the missing server", err)
}
})
t.Run("halting login is allowed and flagged a system server", func(t *testing.T) {
cl := haltClient(t, mcServer("login", v1alpha1.DesiredRunning, v1alpha1.PhaseRunning))
out, err := haltServer(ctx, cl, haltNS, "login")
if err != nil {
t.Fatalf("haltServer(login): %v", err)
}
if !out.system {
t.Error("system = false, want true for the login front-door server")
}
if got := desiredStateOf(t, cl, "login"); got != v1alpha1.DesiredStopped {
t.Errorf("desiredState after halt = %q, want Stopped", got)
}
})
}
func TestListServersForHalt(t *testing.T) {
cl := haltClient(t,
mcServer("survival", v1alpha1.DesiredRunning, ""), // no status yet → phase falls back to intent
mcServer("login", v1alpha1.DesiredRunning, ""),
)
got, err := listServersForHalt(context.Background(), cl, haltNS)
if err != nil {
t.Fatalf("listServersForHalt: %v", err)
}
if len(got) != 2 {
t.Fatalf("listed %d servers, want 2", len(got))
}
by := map[string]haltableServer{}
for _, s := range got {
by[s.name] = s
}
if s := by["survival"]; s.system || s.phase != "Running" {
t.Errorf("survival projected %+v, want system=false phase=Running (desired-state fallback)", s)
}
if s := by["login"]; !s.system {
t.Errorf("login projected system=false, want true")
}
}
func TestPerformHalt(t *testing.T) {
ctx := context.Background()
t.Run("halts and records an accountability audit row", func(t *testing.T) {
cl := haltClient(t, mcServer("survival", v1alpha1.DesiredRunning, v1alpha1.PhaseRunning))
f := &fakeOwnerStore{}
out, err := performHalt(ctx, cl, f, haltNS, "survival", "deploybot")
if err != nil {
t.Fatalf("performHalt: %v", err)
}
if out.auditErr != nil {
t.Errorf("auditErr = %v, want nil", out.auditErr)
}
if got := desiredStateOf(t, cl, "survival"); got != v1alpha1.DesiredStopped {
t.Errorf("desiredState after performHalt = %q, want Stopped", got)
}
e, payload := auditOf(t, f)
if e.Action != "break_glass.halt" {
t.Errorf("audit action = %q, want break_glass.halt", e.Action)
}
if e.Actor != "deploybot" {
t.Errorf("audit actor = %q, want deploybot", e.Actor)
}
if payload["server"] != "survival" || payload["system_server"] != false {
t.Errorf("audit payload = %v, want server=survival system_server=false", payload)
}
})
t.Run("a failed audit sink does not fail the halt", func(t *testing.T) {
cl := haltClient(t, mcServer("survival", v1alpha1.DesiredRunning, v1alpha1.PhaseRunning))
f := &fakeOwnerStore{auditErr: context.DeadlineExceeded}
out, err := performHalt(ctx, cl, f, haltNS, "survival", "deploybot")
if err != nil {
t.Fatalf("performHalt returned error on audit failure, want halt to still succeed: %v", err)
}
if out.auditErr == nil {
t.Error("auditErr = nil, want the sink failure surfaced")
}
if got := desiredStateOf(t, cl, "survival"); got != v1alpha1.DesiredStopped {
t.Errorf("desiredState = %q, want Stopped even though the audit failed", got)
}
})
}