diff --git a/internal/platform/identities.go b/internal/platform/identities.go index 8d7baa9..bd4bd75 100644 --- a/internal/platform/identities.go +++ b/internal/platform/identities.go @@ -66,9 +66,11 @@ const ( // Params parameterises the install bundle. Namespaces and the registry location // have safe defaults; VelocityCIDRs has none — see the field comment. type Params struct { - // ControlNamespace is where felis-api/operator/reaper run. Their SAs live here - // and the RoleBindings' subjects reference them here, even though the Roles - // they bind to live in the minecraft (and build) namespaces. + // ControlNamespace is where felis-api/operator run. All three SAs live here and + // the RoleBindings' subjects reference them here, even though the Roles they bind + // to live in the minecraft (and build) namespaces. The reaper CronJob alone runs + // in the Minecraft namespace, because a Pod can only mount PVCs from its own + // namespace and its backup PVC is provisioned there. ControlNamespace string // MinecraftNamespace is where MinecraftServer workloads, their RCON Secrets, // and their world PVCs live. All three identities' minecraft-scoped Roles, and diff --git a/internal/platform/workloads.go b/internal/platform/workloads.go index 74c5676..83e68b1 100644 --- a/internal/platform/workloads.go +++ b/internal/platform/workloads.go @@ -472,8 +472,15 @@ func reaperCronJob(p Params) *batchv1.CronJob { } return &batchv1.CronJob{ - TypeMeta: metav1.TypeMeta{APIVersion: "batch/v1", Kind: "CronJob"}, - ObjectMeta: metav1.ObjectMeta{Name: SAReaper, Namespace: p.ControlNamespace, Labels: labels}, + TypeMeta: metav1.TypeMeta{APIVersion: "batch/v1", Kind: "CronJob"}, + // The CronJob lives in the MINECRAFT namespace: a Pod can only mount PVCs + // from its own namespace and the backup PVC is provisioned there alongside + // the backup Jobs. Placed under ControlNamespace it could never schedule + // (FailedScheduling: persistentvolumeclaim not found) in any stock install; + // the reaper Role/RoleBinding were already minecraft-scoped for the same + // reason, and the minecraft felis-config replica (felis setup) supplies the + // config mount. + ObjectMeta: metav1.ObjectMeta{Name: SAReaper, Namespace: p.MinecraftNamespace, Labels: labels}, Spec: batchv1.CronJobSpec{ Schedule: reaperSchedule, ConcurrencyPolicy: batchv1.ForbidConcurrent, diff --git a/internal/platform/workloads_test.go b/internal/platform/workloads_test.go index c3934fb..55b3a9d 100644 --- a/internal/platform/workloads_test.go +++ b/internal/platform/workloads_test.go @@ -519,8 +519,11 @@ func TestReaperCronJob_Shape(t *testing.T) { if cj.Name != SAReaper { t.Errorf("CronJob name = %q, want %q", cj.Name, SAReaper) } - if cj.Namespace != p.ControlNamespace { - t.Errorf("CronJob namespace = %q, want control ns %q", cj.Namespace, p.ControlNamespace) + // The CronJob must sit where its PVC lives: a Pod cannot mount a PVC across + // namespaces, and the backup PVC is provisioned in the Minecraft namespace. + // (Rendered under ControlNamespace it failed to schedule on a live cluster.) + if cj.Namespace != p.MinecraftNamespace { + t.Errorf("CronJob namespace = %q, want minecraft ns %q (its backup PVC's namespace)", cj.Namespace, p.MinecraftNamespace) } spec := cj.Spec