fix(operator): StatefulSet 显式保留世界卷,同名旧世界卷仍在时建服回 409 world_volume_exists,文档写明孤儿卷查找与单节点约束
This commit is contained in:
11 files changed
+112
-22
No files matched your search
+19
-13
@@ -1731,16 +1731,17 @@ func (f *fakeRestorer) Restore(_ context.Context, name, ref string) error {
|
||||
}
|
||||
|
||||
type fakeCluster struct {
|
||||
byName map[string]*ServerInfo
|
||||
bySub map[string]*ServerInfo
|
||||
list []ServerInfo
|
||||
listErr error
|
||||
desired map[string]v1alpha1.DesiredState
|
||||
created map[string]CreateServerInput // name -> the validated input it was created from
|
||||
patched map[string]ServerSpecPatch // name -> the validated spec patch it received
|
||||
noWorld map[string]bool // server names modeled WITHOUT a world volume (never started / reaped)
|
||||
createErr error
|
||||
pingErr error
|
||||
byName map[string]*ServerInfo
|
||||
bySub map[string]*ServerInfo
|
||||
list []ServerInfo
|
||||
listErr error
|
||||
desired map[string]v1alpha1.DesiredState
|
||||
created map[string]CreateServerInput // name -> the validated input it was created from
|
||||
patched map[string]ServerSpecPatch // name -> the validated spec patch it received
|
||||
noWorld map[string]bool // server names modeled WITHOUT a world volume (never started / reaped)
|
||||
orphanWorld map[string]bool // names with a world volume but no server (CR deleted by hand)
|
||||
createErr error
|
||||
pingErr error
|
||||
// maintErr / wakeErr: what AcquireMaintenance / SetDesiredState(Running)
|
||||
// return for a server (the world-volume lock, internal/maintenance).
|
||||
maintErr map[string]error
|
||||
@@ -1775,10 +1776,15 @@ func (c *fakeCluster) ListServers(_ context.Context) ([]ServerInfo, error) {
|
||||
}
|
||||
func (c *fakeCluster) Ping(_ context.Context) error { return c.pingErr }
|
||||
|
||||
// WorldVolumeExists models the world PVC: present unless the test named the
|
||||
// server in noWorld (never started / already reaped).
|
||||
// WorldVolumeExists models the world PVC: a known server has one unless the test
|
||||
// named it in noWorld (never started / already reaped); an unknown name has one
|
||||
// only when named in orphanWorld (its CR was deleted by hand).
|
||||
func (c *fakeCluster) WorldVolumeExists(_ context.Context, n string) (bool, error) {
|
||||
return !c.noWorld[n], nil
|
||||
if c.orphanWorld[n] {
|
||||
return true, nil
|
||||
}
|
||||
_, known := c.byName[n]
|
||||
return known && !c.noWorld[n], nil
|
||||
}
|
||||
|
||||
func (c *fakeCluster) SetDesiredState(_ context.Context, n string, s v1alpha1.DesiredState) error {
|
||||
|
||||
@@ -274,6 +274,21 @@ func TestCreateServerRejections(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// A CR deleted by hand keeps its world volume; a new server of that
|
||||
// name would mount it and inherit the old world.
|
||||
name: "world volume left by a deleted server",
|
||||
body: validCreateBody,
|
||||
setup: func(_ *fakeRepo, cl *fakeCluster) {
|
||||
cl.orphanWorld = map[string]bool{"survival": true}
|
||||
},
|
||||
wantCode: http.StatusConflict, wantErr: "world_volume_exists",
|
||||
check: func(t *testing.T, repo *fakeRepo, _ *fakeCluster) {
|
||||
if repo.seeded["survival"] {
|
||||
t.Error("a create refused over a leftover volume must not seed a servers row")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -475,6 +475,20 @@ func (a *API) handleCreateServer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// A server deleted outside the reaper (kubectl delete) leaves its world
|
||||
// volume behind: the StatefulSet retains claims on delete. A new server of
|
||||
// the same name would mount that claim and hand the old world to its new
|
||||
// owner, so the name stays taken until an operator removes the volume.
|
||||
switch exists, err := a.Cluster.WorldVolumeExists(r.Context(), body.Name); {
|
||||
case err != nil:
|
||||
writeError(w, r, err)
|
||||
return
|
||||
case exists:
|
||||
writeError(w, r, newError(http.StatusConflict, "world_volume_exists",
|
||||
"the world volume of an earlier server named %q still exists; delete it or choose another name", body.Name))
|
||||
return
|
||||
}
|
||||
|
||||
// Seed the business rows FIRST (servers + alias). ClaimServer needs the row,
|
||||
// so a CRD-only server would be unclaimable. PG-first means a later CRD
|
||||
// failure leaves a claimable ghost row — acceptable, not transactional.
|
||||
|
||||
@@ -320,6 +320,16 @@ func buildStatefulSet(server *v1alpha1.MinecraftServer, replicas int32, felisIma
|
||||
},
|
||||
},
|
||||
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{pvc},
|
||||
// The world outlives its StatefulSet: deleting the CR (and with it,
|
||||
// by owner reference, the StatefulSet) keeps the claim, so a CR that
|
||||
// comes back under the same name mounts the same world. The reaper is
|
||||
// the one path that deletes a world, after its final backup. Stated
|
||||
// here although it is the API default, so the semantics never ride on
|
||||
// a default.
|
||||
PersistentVolumeClaimRetentionPolicy: &appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy{
|
||||
WhenDeleted: appsv1.RetainPersistentVolumeClaimRetentionPolicyType,
|
||||
WhenScaled: appsv1.RetainPersistentVolumeClaimRetentionPolicyType,
|
||||
},
|
||||
},
|
||||
}
|
||||
return sts, nil
|
||||
|
||||
@@ -54,3 +54,17 @@ func TestGameContainerProbes(t *testing.T) {
|
||||
t.Fatalf("login-gate startup = %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting a server's StatefulSet, or scaling it to zero, keeps the world claim.
|
||||
func TestWorldClaimOutlivesTheStatefulSet(t *testing.T) {
|
||||
srv := &v1alpha1.MinecraftServer{ObjectMeta: metav1.ObjectMeta{Name: "survival", Namespace: "minecraft"}}
|
||||
srv.Spec.Image = "itzg/minecraft-server:java21"
|
||||
sts, err := buildStatefulSet(srv, 1, "felis:test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := sts.Spec.PersistentVolumeClaimRetentionPolicy
|
||||
if p == nil || p.WhenDeleted != "Retain" || p.WhenScaled != "Retain" {
|
||||
t.Fatalf("retention policy = %+v, want Retain on delete and on scale", p)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user