feat: implement C.4 — hermetic container e2e gate + native release build
Separate test/e2e Go module drives the shipped deploy/docker-compose.yml (plus a test-only override: self-signed cert, low ports, isolated compose project) against a fake DNS zone (CoreDNS) and an smtp-sink MX, exactly as an administrator and their applications would over HTTP/SMTP — covering the class of failure unit tests can't see (container wiring). Positive path: setup -> login -> domain -> DKIM record published into the fake zone -> application -> SMTP AUTH send -> DKIM verified against the DNS-published key -> send-log queued->sent. Negative: no-AUTH/unauthenticated relay, sender/login mismatch, L1 (anvil) and L2 (panel) rate limits, journal-milter fail-open, SELFPOST_HOSTNAME gate, session survives docker restart. release.yml moves off qemu to a native per-arch build (amd64/arm64), each gated by this suite before its tag is pushed and merged into the version manifest. Verified green on selfpost.mixfed.ru via `make e2e`; go vet/gofmt clean in both modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// wantRunning are the supervisord programs that must be RUNNING once the
|
||||
// container is up (build/supervisord.conf). postfix-reload is deliberately
|
||||
// excluded: autostart=false, its healthy resting state is STOPPED.
|
||||
var wantRunning = []string{"opendkim", "panel", "postfix", "cert-reload", "logrotate"}
|
||||
|
||||
// checkSupervisorProcesses shells into the container directly (not through the
|
||||
// panel's /status page) so it works before an administrator account even
|
||||
// exists — this is the first thing the harness checks after `docker compose
|
||||
// up` (plan C.4). Programs take a moment to leave STARTING right after the
|
||||
// container starts, so this polls rather than checking once.
|
||||
func checkSupervisorProcesses(s *stack) error {
|
||||
var lastErr error
|
||||
err := waitFor("all supervised programs to reach their steady state", 20*time.Second, 500*time.Millisecond, func() (bool, error) {
|
||||
out, execErr := s.execIn("selfpost", "supervisorctl", "-c", "/etc/supervisor/supervisord.conf", "status")
|
||||
// supervisorctl exits non-zero when any program is not RUNNING, which
|
||||
// is expected for postfix-reload — parse the output regardless of exit
|
||||
// status.
|
||||
states := parseSupervisorStatus(out)
|
||||
if len(states) == 0 {
|
||||
lastErr = fmt.Errorf("supervisorctl status produced nothing to parse: %v\n%s", execErr, out)
|
||||
return false, lastErr
|
||||
}
|
||||
for _, name := range wantRunning {
|
||||
state, ok := states[name]
|
||||
if !ok {
|
||||
lastErr = fmt.Errorf("program %q not reported by supervisorctl:\n%s", name, out)
|
||||
return false, lastErr
|
||||
}
|
||||
if state != "RUNNING" {
|
||||
lastErr = fmt.Errorf("program %q is %s, want RUNNING:\n%s", name, state, out)
|
||||
return false, lastErr
|
||||
}
|
||||
}
|
||||
if state, ok := states["postfix-reload"]; ok && state != "STOPPED" {
|
||||
lastErr = fmt.Errorf("program postfix-reload is %s, want STOPPED (autostart=false):\n%s", state, out)
|
||||
return false, lastErr
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func parseSupervisorStatus(out string) map[string]string {
|
||||
states := make(map[string]string)
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
states[fields[0]] = fields[1]
|
||||
}
|
||||
return states
|
||||
}
|
||||
Reference in New Issue
Block a user