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
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -75,3 +76,48 @@ func TestResolveWorldDir(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The pre-reap warner resolves the owner's VERIFIED email and hands the notice
|
||||
// to the mailer. Every failure (no verified address, relay refusal) returns an
|
||||
// error so the reaper retries on its next run instead of stamping a notice
|
||||
// nobody received.
|
||||
func TestMailWarner(t *testing.T) {
|
||||
lookup := func(email string, err error) func(context.Context, string) (string, error) {
|
||||
return func(context.Context, string) (string, error) { return email, err }
|
||||
}
|
||||
|
||||
n := &captureNotifier{}
|
||||
w := &mailWarner{lookupEmail: lookup("[email protected]", nil), notifier: n}
|
||||
if err := w.Warn(context.Background(), "u1", "survival", "3d"); err != nil {
|
||||
t.Fatalf("Warn: %v", err)
|
||||
}
|
||||
if n.email != "[email protected]" || !strings.Contains(n.subject, "survival") || !strings.Contains(n.subject, "3d") {
|
||||
t.Fatalf("notice envelope = (%q, %q)", n.email, n.subject)
|
||||
}
|
||||
if !strings.Contains(n.body, "survival") || !strings.Contains(n.body, "3d") {
|
||||
t.Fatalf("body missing server/remaining:\n%s", n.body)
|
||||
}
|
||||
|
||||
w = &mailWarner{lookupEmail: lookup("", errors.New("owner u2 has no verified email")), notifier: n}
|
||||
if err := w.Warn(context.Background(), "u2", "survival", "3d"); err == nil || !strings.Contains(err.Error(), "verified email") {
|
||||
t.Fatalf("unverified owner = %v, want the lookup error surfaced", err)
|
||||
}
|
||||
|
||||
w = &mailWarner{lookupEmail: lookup("[email protected]", nil), notifier: &captureNotifier{err: errors.New("relay down")}}
|
||||
if err := w.Warn(context.Background(), "u1", "survival", "3d"); err == nil || !strings.Contains(err.Error(), "relay down") {
|
||||
t.Fatalf("relay failure = %v, want it surfaced", err)
|
||||
}
|
||||
}
|
||||
|
||||
type captureNotifier struct {
|
||||
email, subject, body string
|
||||
err error
|
||||
}
|
||||
|
||||
func (n *captureNotifier) SendNotice(_ context.Context, email, subject, body string) error {
|
||||
if n.err != nil {
|
||||
return n.err
|
||||
}
|
||||
n.email, n.subject, n.body = email, subject, body
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user