Unverified Commit 7b91f7c6 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(reaper): 只清理备份库的 CronJob 用命名空间默认 SA,e2e 实跑一次 reaper

parent 572d12d5
Loading
Loading
Loading
Loading
+14 −0
Changes for deploy/e2e_check.sh: 14 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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
+55 −0
Changes for internal/platform/bundle_test.go: 55 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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)
		}
	}
}
+15 −4
Changes for internal/platform/workloads.go: 15 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -825,11 +825,19 @@ 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(),
@@ -839,7 +847,10 @@ func reaperPodSpec(p Params, container corev1.Container, volumes []corev1.Volume
	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
+3 −0
Changes for internal/platform/workloads_test.go: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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")
	}