release: 0.1.0: Phases 0-11: panel, Postfix/DKIM, milter, backup, Docker image
Initial feature-complete implementation of the v1.0 specification (phases 0-11 of `docs/implementation-plan.md`). Added: - Panel (Go, single static binary) with SQLite persistence, one-time - Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and - Application (sender identity) management: SASL credentials via `sasldb2`, - Full Postfix relay config generated from env at container start: SMTPS 465, - Journal milter (pure Go, `go-milter`) recording every send to `send_log`; … (6 more in CHANGELOG) Full detail: CHANGELOG.md (0.1.0).
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