fix(reaper): deliver pre-reap warnings for real — and never fake a delivery
The §18 warning path had no delivery channel at all: no Warner implementation existed, `felis reaper` passed nil, and maybeWarn still stamped warned_3d_at/ warned_1d_at and counted `warned=N`. So every owned server was silently reaped 15 days after its last join with no notice, and the operator's only feedback said warnings were sent. Two changes close that: - Honest stamps: warned_* now records a DELIVERED notice. A nil Warner logs `warning suppressed — no warner wired` and does NOT stamp; a delivery error logs and retries on the next daily run (bounded by the warning window). The stamps are no longer burned by notices nobody received. - A real channel: mail.SendNotice (the second and last message shape the mail package sends) plus a mailWarner that resolves the owner's VERIFIED email and mails the notice through the configured [smtp] relay. `felis reaper` wires it when [smtp] is set (same password_ref convention as felis-api) and prints exactly what happens when it is not. Plumbing so the in-cluster CronJob can actually reach the relay: the reaper pod gets the optional FELIS_SMTP_PASSWORD env (same Secret as felis-api), and the "configure email" screen now refreshes the minecraft-namespace mirrors of felis-smtp AND felis-config (a secretKeyRef is namespace-local, and the config mirror is what carries [smtp] into the reaper's own config). `felis setup`'s replica list gains felis-smtp for fresh installs. Tests: the delivered/retried/suppressed matrix in internal/reaper (the old "stamp advances on failure" contract is deliberately replaced), the notice message shape, the warner's resolve/send/failure paths, and the CronJob's optional-secret env. docs/troubleshooting.md §10 now states the real semantics.
This commit is contained in:
12 files changed
+375
-27
No files matched your search
@@ -536,6 +536,20 @@ func reaperCronJob(p Params) *batchv1.CronJob {
|
||||
"--config", configFilePath,
|
||||
"--worlds-root", worldsMountPath,
|
||||
},
|
||||
// The [smtp] relay password for pre-reap warning emails — same optional
|
||||
// Secret felis-api reads. Namespace caveat: a secretKeyRef is
|
||||
// namespace-local, so this resolves against the minecraft-ns felis-smtp
|
||||
// mirror that the "configure email" screen refreshes (the felis-config
|
||||
// mirror it also refreshes is what puts [smtp] in this pod's config).
|
||||
// Absent Secret ⇒ empty env ⇒ the reaper logs suppressed warnings
|
||||
// instead of stamping them (never a failed pod).
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: SMTPPasswordEnv, ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: SMTPSecretName},
|
||||
Key: SMTPSecretPasswordKey,
|
||||
Optional: boolPtr(true),
|
||||
}}},
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{Name: configVolume, MountPath: configMountPath, ReadOnly: true},
|
||||
{Name: worldsVolume, MountPath: worldsMountPath, ReadOnly: true},
|
||||
|
||||
@@ -729,6 +729,19 @@ func TestReaperCronJob_Shape(t *testing.T) {
|
||||
t.Errorf("reaper image = %q, want FelisImage %q", c.Image, p.FelisImage)
|
||||
}
|
||||
|
||||
// The relay password for pre-reap warning emails: same optional Secret as
|
||||
// felis-api, resolved against the minecraft-ns mirror. Optional so an install
|
||||
// without SMTP still starts (the reaper then logs suppressed warnings).
|
||||
smtpEnv := envVar(c.Env, SMTPPasswordEnv)
|
||||
if smtpEnv == nil || smtpEnv.ValueFrom == nil || smtpEnv.ValueFrom.SecretKeyRef == nil {
|
||||
t.Fatalf("reaper must wire %s from a secretKeyRef", SMTPPasswordEnv)
|
||||
}
|
||||
if ref := smtpEnv.ValueFrom.SecretKeyRef; ref.Name != SMTPSecretName || ref.Key != SMTPSecretPasswordKey {
|
||||
t.Errorf("reaper %s ref = %s/%s, want %s/%s", SMTPPasswordEnv, ref.Name, ref.Key, SMTPSecretName, SMTPSecretPasswordKey)
|
||||
} else if ref.Optional == nil || !*ref.Optional {
|
||||
t.Errorf("reaper %s secretKeyRef must be optional", SMTPPasswordEnv)
|
||||
}
|
||||
|
||||
// config: Secret, mounted read-only (it carries the DB URL).
|
||||
cfgVol := volumeByName(ps.Volumes, configVolume)
|
||||
if cfgVol == nil || cfgVol.Secret == nil || cfgVol.Secret.SecretName != configSecretName {
|
||||
|
||||
Reference in new issue
Block a user