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
|
fi
|
||||||
rm -rf "$bundle_dir"
|
rm -rf "$bundle_dir"
|
||||||
fi
|
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.
|
# A release may predate a timer; what this commit installs has them all.
|
||||||
if [ "$phase" != release ]; then
|
if [ "$phase" != release ]; then
|
||||||
for timer in felis-db-backup.timer felis-watchdog.timer felis-update-check.timer; do
|
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")
|
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
|
// 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
|
// 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
|
// 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
|
// the node that actually holds the worlds hostPath on a multi-node cluster.
|
||||||
// retention-only shape gets no service account token: it never calls the API.
|
//
|
||||||
|
// 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 {
|
func reaperPodSpec(p Params, container corev1.Container, volumes []corev1.Volume) corev1.PodSpec {
|
||||||
spec := corev1.PodSpec{
|
spec := corev1.PodSpec{
|
||||||
ServiceAccountName: SAReaper,
|
PriorityClassName: controlPlanePriorityName,
|
||||||
PriorityClassName: controlPlanePriorityName,
|
RestartPolicy: corev1.RestartPolicyNever,
|
||||||
RestartPolicy: corev1.RestartPolicyNever,
|
SecurityContext: reaperPodSecurityContext(),
|
||||||
SecurityContext: reaperPodSecurityContext(),
|
Containers: []corev1.Container{container},
|
||||||
Containers: []corev1.Container{container},
|
Volumes: volumes,
|
||||||
Volumes: volumes,
|
|
||||||
}
|
}
|
||||||
if p.ReaperNode != "" {
|
if p.ReaperNode != "" {
|
||||||
spec.NodeSelector = map[string]string{"kubernetes.io/hostname": 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)
|
spec.AutomountServiceAccountToken = boolPtr(false)
|
||||||
}
|
}
|
||||||
return spec
|
return spec
|
||||||
|
|||||||
@@ -873,6 +873,9 @@ func TestReaperCronJob_RetentionOnlyShape(t *testing.T) {
|
|||||||
if ps.AutomountServiceAccountToken == nil || *ps.AutomountServiceAccountToken {
|
if ps.AutomountServiceAccountToken == nil || *ps.AutomountServiceAccountToken {
|
||||||
t.Error("a retention-only run never calls the API and must not mount a service account token")
|
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" {
|
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")
|
t.Error("it reads back and deletes archives other identities wrote, so it keeps DAC_OVERRIDE")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user