fix(cli): pin the reaper to its storage node; drop the stale uid-1000 note

Two things in the same surface. --reaper-node is the supported multi-node
answer: the rendered CronJob's pod gets a kubernetes.io/hostname selector, so
it reads the hostPath on the node that actually holds the worlds instead of
possibly scheduling where it is empty (naming a node without
--worlds-host-path is fail-loud). And the render note still told operators to
grant uid-1000 traverse / setfacl after #35 moved every world executor to
root+DAC_OVERRIDE — it now states that fact instead of the obsolete ritual.
This commit is contained in:
Lemon-miaow committed 2026-09-23 06:58:29 +08:00
1 parent a31eca65c3
commit daf760220b
5 files changed
+112 -26

No files matched your search

+25 -13
View File
@@ -38,11 +38,11 @@ import (
// (<pv-name>_<ns>_<pvc-name>, read from the live PVC), so pointing
// --worlds-host-path at /var/lib/rancher/k3s/storage works on a default install —
// see the WorldsHostPath field doc. Whether the tar finds a world still depends on
// the hosting node, and is not provable without a cluster. No nodeSelector is set:
// the single-node starter pins
// the worlds to one node implicitly; a multi-node deployment MUST add one (or the
// CronJob could schedule on a node where the hostPath is empty) — a hazard left on
// record here until multi-node retention is built.
// the hosting node, and is not provable without a cluster. No nodeSelector is set
// unless ReaperNode names one: the single-node starter pins the worlds to one node
// implicitly, while a multi-node deployment passes --reaper-node (rendered as a
// kubernetes.io/hostname selector) or the CronJob could schedule on a node where
// the hostPath is empty.
const (
// configSecretName / serviceTokenSecretName are referenced BY NAME and NEVER
// rendered into the bundle: felis.toml carries the database URL (a credential)
@@ -605,14 +605,7 @@ func reaperCronJob(p Params) *batchv1.CronJob {
ActiveDeadlineSeconds: int64Ptr(reaperActiveDeadlineSeconds),
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: labels},
Spec: corev1.PodSpec{
ServiceAccountName: SAReaper,
PriorityClassName: controlPlanePriorityName,
RestartPolicy: corev1.RestartPolicyNever,
SecurityContext: reaperPodSecurityContext(),
Containers: []corev1.Container{container},
Volumes: volumes,
},
Spec: reaperPodSpec(p, container, volumes),
},
},
},
@@ -620,6 +613,25 @@ func reaperCronJob(p Params) *batchv1.CronJob {
}
}
// reaperPodSpec is the reaper Job's pod template. It lives apart from the CronJob
// literal only so the optional node pin is one visible branch: with ReaperNode
// set the pod carries a kubernetes.io/hostname selector, keeping the reaper on
// the node that actually holds the worlds hostPath on a multi-node cluster.
func reaperPodSpec(p Params, container corev1.Container, volumes []corev1.Volume) corev1.PodSpec {
spec := corev1.PodSpec{
ServiceAccountName: SAReaper,
PriorityClassName: controlPlanePriorityName,
RestartPolicy: corev1.RestartPolicyNever,
SecurityContext: reaperPodSecurityContext(),
Containers: []corev1.Container{container},
Volumes: volumes,
}
if p.ReaperNode != "" {
spec.NodeSelector = map[string]string{"kubernetes.io/hostname": p.ReaperNode}
}
return spec
}
// controlPlaneDeployment assembles a single-replica control-plane Deployment. The
// Deployment, its selector, and the pod template all carry
// controlPlanePodLabels(component) so the three agree (a selector mismatch would