d43a86c4bd
test / test (push) Has been cancelled
Capture docker inspect, /data modes, supervisorctl, and entrypoint markers on failure (stderr DEBUG816647 lines + debug-816647.log) so release CI can show why valid_hostname_starts / startup_processes_running die with empty logs. Co-Authored-By: Composer <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
94 lines
3.6 KiB
Go
94 lines
3.6 KiB
Go
package e2e
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"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
|
|
})
|
|
if err != nil {
|
|
logs := s.logs("selfpost")
|
|
// #region agent log
|
|
ps, _ := s.compose("ps", "-a", "--format", "json")
|
|
inspectOut, _ := exec.Command("docker", "ps", "-a", "--filter", "name=selfpost-e2e",
|
|
"--format", "{{.Names}} {{.Status}} {{.ID}}").CombinedOutput()
|
|
dataPath := filepath.Join(s.stageDir, "data")
|
|
dataMode := ""
|
|
if fi, e := os.Stat(dataPath); e == nil {
|
|
dataMode = fmt.Sprintf("%04o", fi.Mode().Perm())
|
|
}
|
|
entrypointSnippet, _ := exec.Command("docker", "run", "--rm", "--entrypoint", "sh", "selfpost:e2e",
|
|
"-c", "grep -n 'chmod 755 /data\\|SELFPOST_HOSTNAME is not set' /usr/local/bin/entrypoint.sh | head -20").CombinedOutput()
|
|
agentDebugLog("H4", "process_check.go:fail", "compose selfpost not ready", map[string]any{
|
|
"waitErr": err.Error(),
|
|
"logsLen": len(logs),
|
|
"logsHead": truncateForDebug(logs, 4000),
|
|
"composePs": truncateForDebug(string(ps), 2000),
|
|
"dockerPs": strings.TrimSpace(string(inspectOut)),
|
|
"hostDataMode": dataMode,
|
|
"entrypointSnippets": strings.TrimSpace(string(entrypointSnippet)),
|
|
})
|
|
agentDebugLog("H5", "process_check.go:fail", "image entrypoint markers", map[string]any{
|
|
"snippet": strings.TrimSpace(string(entrypointSnippet)),
|
|
})
|
|
// #endregion
|
|
return fmt.Errorf("%w\n==== selfpost logs ====\n%s\n==== docker ps ====\n%s\n==== entrypoint markers ====\n%s",
|
|
err, logs, inspectOut, entrypointSnippet)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|