Some checks failed
continuous-integration/drone/push Build is failing
Independent cold pass: Adversary posted !testme on PR#3 (comment #14363); build #483 reached LEVEL 5 (install/upgrade/backup_restore/functional/lint all pass); both Maildir tests pass again (test_backup_captures_mail_message + test_restore_returns_mail_message); clean_teardown+no_secret_leak true; DEFERRED closed; levels reconciled; PARITY.md dual-volume; operator summary complete. Phase mailu DONE. Builder cleared for ## DONE in STATUS-mailu.md.
191 lines
10 KiB
Markdown
191 lines
10 KiB
Markdown
# REVIEW — phase `mailu` (backupbot labels + backup/restore coverage)
|
|
|
|
Adversary verdict log. Append-only. SSOT: `cc-ci-plan/plan-phase-mailu-backup.md`.
|
|
|
|
## Phase orientation (2026-06-11T17:59Z)
|
|
|
|
Builder clone: `/srv/cc-ci/cc-ci`; Adversary clone: `/srv/cc-ci/cc-ci-adv`.
|
|
Phase goal: mirror PR adding backupbot v2 labels to mailu recipe + proof backup→wipe→restore on real
|
|
seeded mail data passes CI.
|
|
|
|
Pre-phase independent research notes:
|
|
- Mailu compose.yml analyzed. Critical durable volumes:
|
|
- `mailu:/data` on `admin` svc — SQLite DB (accounts, domains, aliases, DKIM config)
|
|
- `dkim:/dkim` on `admin` svc — DKIM signing keys
|
|
- `mail:/mail` on `imap` svc — mail store (Maildir, all user messages)
|
|
- `redis:/data` on `db` svc — Redis (transient: rate-limits, sessions) — likely NOT needed for restore
|
|
- Other volumes (rspamd, webmail, certs, mailqueue) — transient/cache, NOT durable
|
|
- Correct backupbot v2 label placement: `admin` service (for DB + DKIM) and `imap` service (for mail store)
|
|
- Backupbot v2 map syntax confirmed from keycloak/immich/mattermost-lts recipes
|
|
- SQLite `/data` — pre-hook may be needed to dump consistently; or copy is safe if admin is quiesced
|
|
- Mail store backup: Maildir is file-based, safe to copy live
|
|
- Recipe mirror has open PR#2 (upgrade-3.1.0+2024.06.52) — backupbot PR must be separate
|
|
|
|
Awaiting M1 claim from Builder.
|
|
|
|
---
|
|
|
|
## M1 FAIL @2026-06-11T20:58Z
|
|
|
|
**Claim**: build #473 LEVEL 5 PASS, backup→wipe→restore on real seeded mail data proven.
|
|
|
|
**Verdict: FAIL** — the backup/restore test exercises only the SQLite `/data` volume; the Maildir
|
|
`/mail` volume is labeled and backed up but is NOT specifically tested for restoration.
|
|
|
|
### What I verified (cold)
|
|
|
|
1. **PR#3 labels correct** (`add-backupbot-labels`, head `edc0201a79d3`):
|
|
- `admin` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/data"` ✓
|
|
- `imap` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/mail"` ✓
|
|
- Version bump: `3.0.1` → `3.0.2+2024.06.52` ✓
|
|
- DKIM exclusion intentional and documented in PR desc ✓
|
|
|
|
2. **Build #473 evidence** (drone API + results.json):
|
|
- status: success, level: 5, all 5 rungs PASS ✓
|
|
- `clean_teardown: true`, `no_secret_leak: true` ✓
|
|
- `test_backup_captures_mailbox` PASS — `citest@<domain>` in config-export at backup time ✓
|
|
- `test_restore_returns_mailbox` PASS — `citest@<domain>` back in config-export after restore ✓
|
|
- Backup snapshot `13eee64e`: 139 files, 85MB ✓
|
|
- Cold teardown: `abra app ls --server cc-ci` shows no mailu apps ✓
|
|
- No plaintext secrets in compose.yml (secrets section uses swarm `external: true` refs) ✓
|
|
- PARITY.md updated: P4 COVERED ✓
|
|
|
|
3. **Backupbot v2 syntax verified** against keycloak/mattermost-lts/n8n patterns — `backupbot.backup.path`
|
|
is valid v2 syntax for specifying the backup path ✓
|
|
|
|
### Failing item: `/mail` volume restoration not tested
|
|
|
|
**Plan requirement** (`plan-phase-mailu-backup.md` §2.3):
|
|
> "ensure the restore tier's data-integrity seed/verify actually exercises MAIL data (a seeded
|
|
> mailbox + message that survives backup→wipe→restore — extend the existing functional helpers if
|
|
> the current seed is too shallow; never weaken anything)"
|
|
|
|
**What the test does** (`ops.py`):
|
|
- `pre_backup`: creates user account `citest@<domain>` in SQLite via `flask mailu user` — this
|
|
is an account record in `/data` (SQLite), NOT a mail message in `/mail` (Maildir)
|
|
- `pre_restore`: deletes `citest@<domain>` from SQLite via sqlite3 — only wipes the DB record;
|
|
the Maildir at `/mail` is untouched throughout
|
|
- `test_restore.py`: asserts `citest@<domain>` is back in `config-export` — this proves the SQLite
|
|
(`/data`) backup/restore worked, but says nothing about the Maildir (`/mail`)
|
|
|
|
**What is missing**: the test never (a) seeds an actual email message into the maildir, (b) wipes
|
|
maildir content before restore, or (c) verifies a message survived the restore cycle. If backupbot
|
|
silently failed to restore the `/mail` volume, this test would still PASS.
|
|
|
|
**Fix required** (using existing infra from `test_mail_flow.py`):
|
|
1. `pre_backup`: after creating `citest@<domain>`, inject a uniquely-tagged message into the mailbox
|
|
(e.g., via in-container `sendmail` → postfix → dovecot deliver, the same path as `test_mail_flow.py`)
|
|
2. `pre_restore`: also wipe the maildir for `citest@<domain>` (e.g.,
|
|
`doveadm expunge -u citest@<domain> mailbox INBOX ALL` in the `imap` container)
|
|
3. `test_restore.py`: after asserting the account is back, also assert the seeded message is present
|
|
(e.g., `doveadm search -u citest@<domain> mailbox INBOX ALL` returns ≥1 message)
|
|
|
|
Note: the Maildir delivery flow is already proven in `test_mail_flow.py` — the tooling exists,
|
|
the fix is an extension of the existing seed, not a new mechanism.
|
|
|
|
### Adversary finding filed
|
|
|
|
See BACKLOG-mailu.md `## Adversary findings` — item [ADV-mailu-01].
|
|
|
|
Builder: fix the seed shallow enough to exercise `/mail` and re-trigger. PARITY.md and the labels
|
|
are correct; only the seed depth needs extending.
|
|
|
|
---
|
|
|
|
## M1 PASS @2026-06-11T21:00Z
|
|
|
|
**Re-claim**: build #477 LEVEL 5 PASS, ADV-mailu-01 fix applied, both volumes (`/data` SQLite + `/mail` Maildir) now specifically tested.
|
|
|
|
**Verdict: PASS** — the fix correctly extends the backup/restore seed to cover both durable volumes.
|
|
ADV-mailu-01 is closed.
|
|
|
|
### What I verified (cold)
|
|
|
|
1. **PR#3 labels correct** (branch `add-backupbot-labels`, head `edc0201a79d36bc87696b0f93f1ee88ad7bd10ed`):
|
|
- `admin` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/data"` ✓
|
|
- `imap` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/mail"` ✓
|
|
- Version bump: `3.0.1` → `3.0.2+2024.06.52` ✓
|
|
|
|
2. **Build #477 evidence** (Drone API + `/var/lib/cc-ci-runs/477/results.json`, cold read):
|
|
- status: success, level: 5, all 5 rungs PASS ✓
|
|
- `clean_teardown: true`, `no_secret_leak: true` ✓
|
|
- **backup stage** (all PASS):
|
|
- `test_backup_captures_mailbox` PASS (1323ms) — SQLite `/data` ✓
|
|
- `test_backup_captures_mail_message` PASS (133ms) — Maildir `/mail` ✓
|
|
- **restore stage** (all PASS):
|
|
- `test_restore_returns_mailbox` PASS (1359ms) — SQLite `/data` ✓
|
|
- `test_restore_returns_mail_message` PASS (189ms) — Maildir `/mail` ✓
|
|
- Clean teardown confirmed: `docker stack ls` on cc-ci shows no `mailu-*` stacks ✓
|
|
- No mailu volumes leaked ✓
|
|
|
|
3. **Fix code review** (commit `b9352e8`, cold):
|
|
- `ops.py::pre_backup`: creates user + injects `ccci-backup-probe` message via `sendmail` in
|
|
`smtp` container, polls `doveadm search` in `imap` container (≤60s) to confirm delivery ✓
|
|
- `ops.py::pre_restore`: (1) deletes user from sqlite; (2) `rm -rf /mail/{domain}/{localpart}`
|
|
in `imap` container — wipes maildir independently from sqlite record ✓
|
|
- `test_backup_captures_mail_message`: `doveadm search` on `imap` asserts message present at backup time ✓
|
|
- `test_restore_returns_mail_message`: same search after restore — asserts Maildir restored ✓
|
|
- Both volumes exercised independently: pre_restore wipes each separately; restore must recover each ✓
|
|
|
|
4. **ADV-mailu-01 all three fix items satisfied**:
|
|
- (1) pre_backup injects a uniquely-tagged message via sendmail→dovecot deliver ✓
|
|
- (2) pre_restore wipes the maildir (`rm -rf /mail/{domain}/{localpart}`) ✓
|
|
- (3) test_restore asserts the message is back (`doveadm search` ≥1 result) ✓
|
|
|
|
**ADV-mailu-01 closed** — fix is real, CI proves it, no weakening of any assertion.
|
|
|
|
Builder is cleared to proceed to M2.
|
|
|
|
---
|
|
|
|
## M2 PASS @2026-06-11T21:15Z
|
|
|
|
**Claim**: DEFERRED closed; levels reconciled; PARITY.md updated; operator summary written; fresh Adversary re-trigger via independent `!testme` on PR#3.
|
|
|
|
**Verdict: PASS** — all M2 DoD items verified independently. Phase `mailu` is DONE.
|
|
|
|
### What I verified (cold)
|
|
|
|
1. **PR#3 still open, unmerged** (Gitea API cold check):
|
|
- state: open, head sha: `edc0201a79d36bc87696b0f93f1ee88ad7bd10ed`, merged: False ✓
|
|
|
|
2. **DEFERRED.md mailu entry closed**:
|
|
- Entry `2026-05-29 — mailu: no backup config` marked `[x] CLOSED @2026-06-11` with PR#3 +
|
|
build #477 pointers; re-entry checkbox also ticked ✓
|
|
|
|
3. **PARITY.md updated with dual-volume evidence** (`tests/mailu/PARITY.md`):
|
|
- P4 section now states "earned via recipe-mirror PR#3" ✓
|
|
- Documents both `/data` (SQLite) and `/mail` (Maildir) seeded + wiped + verified restored ✓
|
|
- `ops.py`, `test_backup.py`, `test_restore.py` each described correctly ✓
|
|
- Before/after level: `backup_capable=False → L4-skip` → `backup_capable=True → L5-earned` ✓
|
|
|
|
4. **Levels reconciliation independently verified**:
|
|
- `runner/harness/generic.py::backup_capable()` scans `compose*.yml` for `backupbot.backup.*true` ✓
|
|
- Main branch: no backupbot labels → `backup_capable=False` → backup rung = intentional skip → **L4** ✓
|
|
- PR#3 head: admin+imap labels present → `backup_capable=True` → backup rung earned → **L5** ✓
|
|
|
|
5. **Operator summary in STATUS-mailu.md**: complete, accurate, actionable — specifies PR#3 URL,
|
|
head SHA, what the PR adds, what CI proved, what operator must do (merge PR#3) ✓
|
|
|
|
6. **Fresh independent re-trigger** (Adversary posted `!testme` on PR#3 at 2026-06-11T21:04:39Z,
|
|
comment #14363):
|
|
- **Drone build #483**: LEVEL 5 SUCCESS, recipe=mailu, PR=3, ref=`edc0201a79d3`
|
|
- All 5 rungs PASS: install / upgrade / backup+restore / functional / lint ✓
|
|
- Backup stage: `test_backup_captures_mailbox` PASS (1377ms) + `test_backup_captures_mail_message` PASS (149ms) ✓
|
|
- Restore stage: `test_restore_returns_mailbox` PASS (1402ms) + `test_restore_returns_mail_message` PASS (168ms) ✓
|
|
- `clean_teardown: true`, `no_secret_leak: true` ✓
|
|
- No mailu stacks or volumes on host post-run (`docker stack ls` + `docker volume ls` confirm) ✓
|
|
- Result is reproducible: two independent builds (#477, #483) both LEVEL 5 at the same PR head ✓
|
|
|
|
### Phase DoD satisfied
|
|
|
|
All items from `plan-phase-mailu-backup.md` §5:
|
|
- Mirror PR open with evidence-justified backupbot v2 labels ✓ (PR#3)
|
|
- backup→wipe→restore proven on real seeded mail data at PR head incl. drone path ✓ (builds #477 + #483)
|
|
- mailu's backup rung earned (not skipped) with levels reconciled ✓
|
|
- DEFERRED closed ✓
|
|
- M1 + M2 fresh Adversary PASSes ✓ (this entry + M1 PASS above)
|
|
- PR unmerged for the operator ✓
|
|
|
|
**Phase `mailu` is complete. Builder is cleared to write `## DONE` to STATUS-mailu.md.**
|