Comments only, no behaviour changed. Each main.cf/master.cf parameter the
build sets, the wrapper's and the reload hook's reliance on Postfix 3.7.11
(postfix-script, master.5, postconf.5, cleanup_init.c, smtpd_peer.c,
README_FILES), supervisor 4.2.5 (process.py, options.py, events.rst),
logrotate 3.21.0, OpenDKIM's config, Cyrus SASL's sasldb, rspamd 3.11.1
(milter_headers.lua, multimap, the map watcher), Docker Compose, CoreDNS
and certbot now say where that is written. Measurements already there stay
measurements ("date not recorded" where none was); hand-composed fixtures
say so.
Contradictions left for their own commits: C-P31 cleanup's size limit,
C-P42 master outside supervisor's signal, C-V5/V6 startsecs, C-V10,
C-S3/Q3 sasldb written in place, C-Q4/A1 certbot's symlinks, C-R7 rspamd
skips local clients but the signature routine does not, C-P8, C-R4, C-R12,
C-M7, C-E2, C-K8/K9, C-D1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.2 KiB
Go
117 lines
4.2 KiB
Go
package e2e
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// runEntrypoint runs the selfpost:e2e image's entrypoint standalone (no
|
|
// compose, no DNS/sink sidecars — the SELFPOST_HOSTNAME gate in
|
|
// build/entrypoint.sh runs and fails before anything else needs them) with
|
|
// hostnameEnv substituted for SELFPOST_HOSTNAME, and returns its combined
|
|
// output and whether it exited zero.
|
|
func runEntrypoint(t *testing.T, hostnameEnv string) (output string, exitedZero bool) {
|
|
t.Helper()
|
|
dataDir := t.TempDir()
|
|
// Entrypoint also chmod 755 /data; mirror that here so a pre-fix image
|
|
// still gets a traversable bind mount under Go's 0700 TempDir (testing.TempDir
|
|
// uses os.MkdirTemp, src/os/tempfile.go: "created with mode 0o700").
|
|
if err := os.Chmod(dataDir, 0o755); err != nil {
|
|
t.Fatalf("chmod data dir: %v", err)
|
|
}
|
|
|
|
args := []string{
|
|
"run", "--rm",
|
|
"-e", "SELFPOST_HOSTNAME=" + hostnameEnv,
|
|
"-v", dataDir + ":/data",
|
|
"selfpost:e2e",
|
|
}
|
|
cmd := exec.Command("docker", args...)
|
|
cmd.Env = os.Environ()
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err == nil
|
|
}
|
|
|
|
// TestHostnameGate is plan C.4 negative check 7: an empty or syntactically
|
|
// invalid SELFPOST_HOSTNAME must fail the container fast with an explanatory
|
|
// message (plan B.3), never a silent bad fallback. It runs the image directly
|
|
// rather than through the shared stand — the whole point is to exercise
|
|
// entrypoint.sh's gate before anything else in the container has a chance to
|
|
// start, so it does not depend on TestE2E's stack at all.
|
|
func TestHostnameGate(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
hostname string
|
|
want string
|
|
}{
|
|
{"empty", "", "SELFPOST_HOSTNAME is not set"},
|
|
{"scheme_and_port", "https://mail.example.com:465", "must be a bare hostname"},
|
|
{"bare_word_no_dot", "localhost", "fully-qualified domain name"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
out, ok := runEntrypoint(t, c.hostname)
|
|
if ok {
|
|
t.Fatalf("container started with SELFPOST_HOSTNAME=%q, want a fatal exit\noutput:\n%s", c.hostname, out)
|
|
}
|
|
if !strings.Contains(out, c.want) {
|
|
t.Fatalf("SELFPOST_HOSTNAME=%q: output does not mention %q:\n%s", c.hostname, c.want, out)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("valid_hostname_starts", func(t *testing.T) {
|
|
out, ok := runEntrypointBackground(t, "mail.example.test")
|
|
if !ok {
|
|
t.Fatalf("container with a valid SELFPOST_HOSTNAME failed to start:\n%s", out)
|
|
}
|
|
})
|
|
}
|
|
|
|
// runEntrypointBackground starts the container detached and confirms
|
|
// supervisord came up (rather than crash-looping) within a short timeout, then
|
|
// removes it — the positive control for the gate cases above: a valid
|
|
// hostname must not be caught by the same checks.
|
|
func runEntrypointBackground(t *testing.T, hostnameEnv string) (output string, started bool) {
|
|
t.Helper()
|
|
dataDir := t.TempDir()
|
|
if err := os.Chmod(dataDir, 0o755); err != nil {
|
|
t.Fatalf("chmod data dir: %v", err)
|
|
}
|
|
certDir := t.TempDir()
|
|
if err := writeSelfSignedCert(certDir+"/fullchain.pem", certDir+"/privkey.pem"); err != nil {
|
|
t.Fatalf("generate throwaway TLS cert: %v", err)
|
|
}
|
|
name := "selfpost-e2e-hostname-check"
|
|
_ = exec.Command("docker", "rm", "-f", name).Run()
|
|
defer func() {
|
|
// Make bind-mounted /data deletable by Go's TempDir cleanup: the panel
|
|
// leaves setup-token/db/opendkim owned by container UIDs.
|
|
_ = exec.Command("docker", "exec", name, "sh", "-c",
|
|
"chown -R root:root /data && chmod -R a+rwX /data").Run()
|
|
_ = exec.Command("docker", "rm", "-f", name).Run()
|
|
}()
|
|
|
|
up := exec.Command("docker", "run", "-d", "--name", name,
|
|
"-e", "SELFPOST_HOSTNAME="+hostnameEnv,
|
|
"-v", dataDir+":/data",
|
|
"-v", certDir+":/etc/postfix/tls:ro",
|
|
"selfpost:e2e")
|
|
if out, err := up.CombinedOutput(); err != nil {
|
|
return string(out), false
|
|
}
|
|
|
|
err := waitFor("supervisord to report RUNNING processes", 20*time.Second, 300*time.Millisecond, func() (bool, error) {
|
|
out, err := exec.Command("docker", "exec", name, "supervisorctl", "-c", "/etc/supervisor/supervisord.conf", "status").CombinedOutput()
|
|
if strings.Contains(string(out), "RUNNING") {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
})
|
|
logs, _ := exec.Command("docker", "logs", name).CombinedOutput()
|
|
return string(logs), err == nil
|
|
}
|