fix(operator): StatefulSet 显式保留世界卷,同名旧世界卷仍在时建服回 409 world_volume_exists,文档写明孤儿卷查找与单节点约束

This commit is contained in:
Lemon-miaow committed 2026-09-25 19:38:13 +08:00
1 parent b7d4275ca9
commit 925cfcf8f8
11 files changed
+112 -22

No files matched your search

+19 -13
View File
@@ -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 {
+15
View File
@@ -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 {
+14
View File
@@ -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.