fix(reaper): 只清理备份库的 CronJob 用命名空间默认 SA,e2e 实跑一次 reaper
This commit is contained in:
4 files changed
+92
-9
No files matched your search
@@ -119,6 +119,20 @@ if [ "$phase" != release ]; then
|
||||
fi
|
||||
rm -rf "$bundle_dir"
|
||||
fi
|
||||
# The daily reaper is what deletes expired archives. Run it once from its CronJob: the API
|
||||
# accepts a pod template naming a ServiceAccount that does not exist, and only the Job's
|
||||
# pod creation fails, so rendering it proves nothing. A release may carry exactly that bug.
|
||||
if [ "$phase" != release ]; then
|
||||
reaper_job="felis-e2e-reaper-${phase}"
|
||||
"${KUBECTL[@]}" -n minecraft delete job "$reaper_job" --ignore-not-found >/dev/null
|
||||
if "${KUBECTL[@]}" -n minecraft create job --from=cronjob/felis-reaper "$reaper_job" >/dev/null; then
|
||||
check "the reaper CronJob runs to completion" \
|
||||
"${KUBECTL[@]}" -n minecraft wait --for=condition=complete "job/${reaper_job}" --timeout=180s
|
||||
"${KUBECTL[@]}" -n minecraft delete job "$reaper_job" --ignore-not-found >/dev/null
|
||||
else
|
||||
fail "a Job can be created from the reaper CronJob"
|
||||
fi
|
||||
fi
|
||||
# A release may predate a timer; what this commit installs has them all.
|
||||
if [ "$phase" != release ]; then
|
||||
for timer in felis-db-backup.timer felis-watchdog.timer felis-update-check.timer; do
|
||||
|
||||
@@ -226,3 +226,58 @@ func TestRenderYAML_Deterministic(t *testing.T) {
|
||||
t.Error("RenderYAML must be deterministic across calls")
|
||||
}
|
||||
}
|
||||
|
||||
// TestObjects_EveryPodRunsAsARenderedServiceAccount keeps every pod template in the
|
||||
// bundle on a ServiceAccount the bundle renders in the same namespace, or on the
|
||||
// namespace's default one. A pod naming a missing ServiceAccount is never created:
|
||||
// the retention-only reaper once named felis-reaper, which renders only with the
|
||||
// reaping shape, so on every stock install its Jobs timed out without a pod.
|
||||
func TestObjects_EveryPodRunsAsARenderedServiceAccount(t *testing.T) {
|
||||
retention := testParams()
|
||||
retention.BackupPVC, retention.ArchiveLocalPath = "felis-backups", "/backups"
|
||||
reaping := retention
|
||||
reaping.WorldsHostPath = "/var/lib/felis/worlds"
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
p Params
|
||||
pods int
|
||||
}{
|
||||
{"no archive store", testParams(), 4},
|
||||
{"retention only", retention, 5},
|
||||
{"reaping", reaping, 5},
|
||||
} {
|
||||
objs := Objects(c.p)
|
||||
sas := map[string]bool{}
|
||||
for _, o := range objs {
|
||||
if sa, ok := o.(*corev1.ServiceAccount); ok {
|
||||
sas[sa.Namespace+"/"+sa.Name] = true
|
||||
}
|
||||
}
|
||||
pods := 0
|
||||
for _, o := range objs {
|
||||
var spec *corev1.PodSpec
|
||||
switch w := o.(type) {
|
||||
case *appsv1.Deployment:
|
||||
spec = &w.Spec.Template.Spec
|
||||
case *appsv1.StatefulSet:
|
||||
spec = &w.Spec.Template.Spec
|
||||
case *appsv1.DaemonSet:
|
||||
spec = &w.Spec.Template.Spec
|
||||
case *batchv1.Job:
|
||||
spec = &w.Spec.Template.Spec
|
||||
case *batchv1.CronJob:
|
||||
spec = &w.Spec.JobTemplate.Spec.Template.Spec
|
||||
default:
|
||||
continue
|
||||
}
|
||||
pods++
|
||||
if sa := spec.ServiceAccountName; sa != "" && sa != "default" && !sas[o.GetNamespace()+"/"+sa] {
|
||||
t.Errorf("%s: %T %s/%s runs as ServiceAccount %q, which the bundle does not render there",
|
||||
c.name, o, o.GetNamespace(), o.GetName(), sa)
|
||||
}
|
||||
}
|
||||
if pods != c.pods {
|
||||
t.Errorf("%s: %d pod templates checked, want %d", c.name, pods, c.pods)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -825,21 +825,32 @@ func worldsRootPVC(p Params) *corev1.PersistentVolumeClaim {
|
||||
// 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. The
|
||||
// retention-only shape gets no service account token: it never calls the API.
|
||||
// the node that actually holds the worlds hostPath on a multi-node cluster.
|
||||
//
|
||||
// Only the reaping shape runs as SAReaper. The retention-only shape never calls the
|
||||
// API, so it gets no token and runs as the namespace's default ServiceAccount:
|
||||
// felis-reaper is rendered only alongside its Role (rbac.go), and a pod naming a
|
||||
// ServiceAccount that does not exist is never created. Every stock install renders
|
||||
// this shape, and its Jobs used to hit their deadline without a single pod.
|
||||
//
|
||||
// "default" is spelled out. An install upgraded from the broken shape still carries
|
||||
// the deprecated serviceAccount: felis-reaper the API server copied into the
|
||||
// template, and an empty serviceAccountName is filled back in from it.
|
||||
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,
|
||||
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}
|
||||
}
|
||||
if p.WorldsHostPath == "" {
|
||||
if p.WorldsHostPath != "" {
|
||||
spec.ServiceAccountName = SAReaper
|
||||
} else {
|
||||
spec.ServiceAccountName = "default"
|
||||
spec.AutomountServiceAccountToken = boolPtr(false)
|
||||
}
|
||||
return spec
|
||||
|
||||
@@ -873,6 +873,9 @@ func TestReaperCronJob_RetentionOnlyShape(t *testing.T) {
|
||||
if ps.AutomountServiceAccountToken == nil || *ps.AutomountServiceAccountToken {
|
||||
t.Error("a retention-only run never calls the API and must not mount a service account token")
|
||||
}
|
||||
if ps.ServiceAccountName != "default" {
|
||||
t.Errorf("serviceAccountName = %q, want the namespace default spelled out: felis-reaper renders only with the reaping shape, and an empty name is filled back in from an upgraded install's deprecated serviceAccount", ps.ServiceAccountName)
|
||||
}
|
||||
if c.SecurityContext == nil || c.SecurityContext.Capabilities == nil || len(c.SecurityContext.Capabilities.Add) != 1 || c.SecurityContext.Capabilities.Add[0] != "DAC_OVERRIDE" {
|
||||
t.Error("it reads back and deletes archives other identities wrote, so it keeps DAC_OVERRIDE")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user