Phase 1: Docker image, supervisord, three-process cold start
Single bookworm-slim image running opendkim + panel + postfix under supervisord with enforced start ordering (spec 4): - build/Dockerfile: multi-stage static Go build; runtime installs postfix, opendkim, cyrus-sasl, supervisor, logrotate; unprivileged panel user (7.6.8). - build/supervisord.conf: priority ordering opendkim -> panel -> postfix; crashexit event listener terminates the container on any FATAL process. - build/postfix-wrapper.sh: waits for both milter sockets (test -S, 30s timeout) before `postfix start-fg`, exits non-zero on timeout. - panel: HTTP :8080 stub + /healthz, journal-milter socket stub (so the wrapper's readiness probe passes), log-tailer stub; SIGTERM graceful stop. Verified on the dev server: image builds, three processes live, panel serves the stub, wrapper waits for sockets, and an unrecoverable panel failure brings the container down cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# Keep the build context minimal and free of dev-only material.
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
dev
|
||||||
|
docs
|
||||||
|
bin
|
||||||
|
*.exe
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
Makefile
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# SelfPost — single Debian-slim image running postfix + opendkim + panel under
|
||||||
|
# supervisord (spec 4). Build from the repository root:
|
||||||
|
#
|
||||||
|
# docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .
|
||||||
|
|
||||||
|
# ---- build stage -------------------------------------------------------------
|
||||||
|
FROM golang:1.26-bookworm AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Version stamped into both binaries; MUST match the image tag (spec 7.5.A).
|
||||||
|
ARG VERSION=dev
|
||||||
|
|
||||||
|
# Module metadata first for layer caching. No go.sum yet — Phase 1 has no
|
||||||
|
# third-party dependencies.
|
||||||
|
COPY go.mod ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY cmd ./cmd
|
||||||
|
COPY internal ./internal
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
RUN go vet ./... \
|
||||||
|
&& go build -trimpath \
|
||||||
|
-ldflags "-X codeberg.org/mix/selfpost/internal/buildinfo.Version=${VERSION}" \
|
||||||
|
-o /out/panel ./cmd/panel \
|
||||||
|
&& go build -trimpath \
|
||||||
|
-ldflags "-X codeberg.org/mix/selfpost/internal/buildinfo.Version=${VERSION}" \
|
||||||
|
-o /out/selfpost-backup ./cmd/selfpost-backup
|
||||||
|
|
||||||
|
# ---- runtime stage -----------------------------------------------------------
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Preseed Postfix so its install is non-interactive and yields a working
|
||||||
|
# main.cf. The real relay configuration is generated by the panel in Phase 5.
|
||||||
|
RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \
|
||||||
|
&& echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
postfix \
|
||||||
|
opendkim \
|
||||||
|
opendkim-tools \
|
||||||
|
sasl2-bin \
|
||||||
|
libsasl2-modules \
|
||||||
|
supervisor \
|
||||||
|
logrotate \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Postfix logs to a plain file (via its built-in postlogd) so the panel's
|
||||||
|
# log-tailer has something to follow and container logging works without syslog.
|
||||||
|
RUN postconf -e "maillog_file=/var/log/mail.log"
|
||||||
|
|
||||||
|
# Unprivileged user for the panel process (spec 7.6.8).
|
||||||
|
RUN useradd --system --no-create-home --shell /usr/sbin/nologin panel
|
||||||
|
|
||||||
|
# Runtime directories: milter sockets and the consolidated persistent root.
|
||||||
|
RUN mkdir -p /run/opendkim /run/selfpost /data \
|
||||||
|
&& chown opendkim:opendkim /run/opendkim \
|
||||||
|
&& chown panel:panel /run/selfpost /data
|
||||||
|
|
||||||
|
COPY --from=build /out/panel /usr/local/bin/panel
|
||||||
|
COPY --from=build /out/selfpost-backup /usr/local/bin/selfpost-backup
|
||||||
|
|
||||||
|
COPY build/opendkim.conf /etc/opendkim.conf
|
||||||
|
COPY build/postfix-wrapper.sh /usr/local/bin/postfix-wrapper.sh
|
||||||
|
COPY build/crashexit.py /usr/local/bin/crashexit.py
|
||||||
|
COPY build/supervisord.conf /etc/supervisor/supervisord.conf
|
||||||
|
RUN chmod +x /usr/local/bin/postfix-wrapper.sh /usr/local/bin/crashexit.py
|
||||||
|
|
||||||
|
# 8080 panel; 25 outbound; 465/587 inbound submission (used from Phase 5).
|
||||||
|
EXPOSE 8080 25 465 587
|
||||||
|
|
||||||
|
# supervisord is PID 1 and owns process supervision + ordering (spec 4).
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Supervisor event listener: bring the container down on unrecoverable failure.
|
||||||
|
|
||||||
|
If any managed program exhausts its restart retries and enters FATAL, this
|
||||||
|
listener signals supervisord (its parent) to terminate, so the whole container
|
||||||
|
exits and Docker's `restart` policy can recreate it cleanly — rather than
|
||||||
|
lingering "alive" with a dead Postfix/OpenDKIM/panel (spec 4).
|
||||||
|
|
||||||
|
Communication with supervisord uses the event listener protocol over stdin and
|
||||||
|
stdout, so stdout must carry only protocol tokens.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def write_stdout(s):
|
||||||
|
sys.stdout.write(s)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
while True:
|
||||||
|
# Tell supervisord we are ready for the next event.
|
||||||
|
write_stdout("READY\n")
|
||||||
|
|
||||||
|
line = sys.stdin.readline()
|
||||||
|
if not line:
|
||||||
|
return
|
||||||
|
headers = dict(pair.split(":", 1) for pair in line.split())
|
||||||
|
payload_len = int(headers.get("len", 0))
|
||||||
|
if payload_len:
|
||||||
|
sys.stdin.read(payload_len)
|
||||||
|
|
||||||
|
# We only subscribe to PROCESS_STATE_FATAL, so any event means a managed
|
||||||
|
# program can no longer be restarted. Take the container down.
|
||||||
|
sys.stderr.write(
|
||||||
|
"crashexit: a managed process entered FATAL; shutting down container\n"
|
||||||
|
)
|
||||||
|
sys.stderr.flush()
|
||||||
|
os.kill(os.getppid(), signal.SIGTERM)
|
||||||
|
|
||||||
|
write_stdout("RESULT 2\nOK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# OpenDKIM — Phase 1 minimal config.
|
||||||
|
#
|
||||||
|
# The goal here is only that OpenDKIM starts and exposes its milter socket so
|
||||||
|
# the Postfix start wrapper's readiness probe passes. Per-domain signing
|
||||||
|
# (KeyTable / SigningTable, Mode s) is generated and reloaded by the panel in
|
||||||
|
# Phase 3; until keys exist we run in verify-only mode so startup needs no keys.
|
||||||
|
|
||||||
|
Syslog no
|
||||||
|
UMask 007
|
||||||
|
Mode v
|
||||||
|
Socket local:/run/opendkim/opendkim.sock
|
||||||
|
PidFile /run/opendkim/opendkim.pid
|
||||||
|
UserID opendkim
|
||||||
|
Background no
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Postfix start wrapper (spec 4): solves the cold-start race where Postfix would
|
||||||
|
# try to reach the milter sockets before they are listening.
|
||||||
|
#
|
||||||
|
# It blocks until BOTH milter sockets — OpenDKIM and the panel's journal-milter
|
||||||
|
# — are present, then execs `postfix start-fg`. If they are not ready within the
|
||||||
|
# timeout it exits non-zero WITHOUT starting Postfix, so supervisord/Docker see
|
||||||
|
# an explicit startup failure instead of a relay running blind.
|
||||||
|
#
|
||||||
|
# This handles cold start only. Runtime milter failures after a successful start
|
||||||
|
# are governed by fail-open (milter_default_action), configured in Phase 5.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
OPENDKIM_SOCK="${OPENDKIM_SOCKET:-/run/opendkim/opendkim.sock}"
|
||||||
|
JOURNAL_SOCK="${JOURNAL_MILTER_SOCKET:-/run/selfpost/journal.sock}"
|
||||||
|
TIMEOUT="${MILTER_WAIT_TIMEOUT:-30}"
|
||||||
|
INTERVAL=1
|
||||||
|
|
||||||
|
elapsed=0
|
||||||
|
for sock in "$OPENDKIM_SOCK" "$JOURNAL_SOCK"; do
|
||||||
|
while [ ! -S "$sock" ]; do
|
||||||
|
if [ "$elapsed" -ge "$TIMEOUT" ]; then
|
||||||
|
echo "postfix-wrapper: timed out after ${TIMEOUT}s waiting for milter socket $sock" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
elapsed=$((elapsed + INTERVAL))
|
||||||
|
done
|
||||||
|
echo "postfix-wrapper: milter socket ready: $sock"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "postfix-wrapper: both milter sockets ready, starting postfix"
|
||||||
|
exec postfix start-fg
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
; SelfPost process supervision (spec 4).
|
||||||
|
;
|
||||||
|
; Start ORDER is enforced by priority=: OpenDKIM, then the panel (which opens
|
||||||
|
; the journal-milter socket), then the Postfix wrapper — which additionally
|
||||||
|
; blocks until both milter sockets are ready before starting Postfix.
|
||||||
|
;
|
||||||
|
; If any managed process exhausts its restart retries (FATAL), the crashexit
|
||||||
|
; event listener brings the whole container down so Docker's restart policy can
|
||||||
|
; recreate it cleanly, rather than lingering with a dead component.
|
||||||
|
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
pidfile=/run/supervisord.pid
|
||||||
|
loglevel=info
|
||||||
|
|
||||||
|
[unix_http_server]
|
||||||
|
file=/run/supervisor.sock
|
||||||
|
chmod=0700
|
||||||
|
|
||||||
|
[rpcinterface:supervisor]
|
||||||
|
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||||
|
|
||||||
|
[supervisorctl]
|
||||||
|
serverurl=unix:///run/supervisor.sock
|
||||||
|
|
||||||
|
[program:opendkim]
|
||||||
|
command=/usr/sbin/opendkim -f -x /etc/opendkim.conf
|
||||||
|
priority=100
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:panel]
|
||||||
|
command=/usr/local/bin/panel
|
||||||
|
user=panel
|
||||||
|
priority=200
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:postfix]
|
||||||
|
command=/usr/local/bin/postfix-wrapper.sh
|
||||||
|
priority=300
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[eventlistener:crashexit]
|
||||||
|
command=/usr/local/bin/crashexit.py
|
||||||
|
events=PROCESS_STATE_FATAL
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serveHTTP runs the panel's HTTP server until ctx is cancelled. Phase 1 serves
|
||||||
|
// only a placeholder page and a health check; the login flow and real UI arrive
|
||||||
|
// in Phase 2.
|
||||||
|
func serveHTTP(ctx context.Context, addr string) error {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/healthz", handleHealth)
|
||||||
|
mux.HandleFunc("/", handleIndex)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shut the server down cleanly when the process is asked to stop.
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = srv.Shutdown(shutdownCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("http panel listening on %s", addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHealth(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("ok\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(indexHTML))
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexHTML = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>SelfPost</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>SelfPost</h1>
|
||||||
|
<p>The control panel is starting up. Administrator setup and login arrive in a later build.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serveJournalStub opens the journal-milter Unix socket so the Postfix start
|
||||||
|
// wrapper's readiness probe (test -S) succeeds and the cold-start ordering
|
||||||
|
// (spec 4) can be exercised end to end. The real milter protocol handler is
|
||||||
|
// implemented in Phase 6; here connections are simply accepted and closed.
|
||||||
|
func serveJournalStub(ctx context.Context, socketPath string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(socketPath), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Clear a stale socket left behind by an unclean shutdown, otherwise the
|
||||||
|
// listen below fails with "address already in use".
|
||||||
|
if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closing the listener unblocks Accept and unlinks the socket file.
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = ln.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("journal-milter stub listening on %s", socketPath)
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil // expected during shutdown
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tailMailLog is the Phase 1 placeholder for the log-tailer role. In Phase 6 it
|
||||||
|
// will follow mail.log and reconcile send-log delivery statuses by queue-id;
|
||||||
|
// for now it just idles until shutdown so the role is present in the process
|
||||||
|
// tree and its wiring is exercised.
|
||||||
|
func tailMailLog(ctx context.Context, path string) error {
|
||||||
|
log.Printf("log-tailer stub active (will follow %s in Phase 6)", path)
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+85
-3
@@ -2,14 +2,20 @@
|
|||||||
// single binary combines several roles (spec 7.1): the HTTP panel server,
|
// single binary combines several roles (spec 7.1): the HTTP panel server,
|
||||||
// the journal-milter, the mail.log tailer and the rate-limit checks.
|
// the journal-milter, the mail.log tailer and the rate-limit checks.
|
||||||
//
|
//
|
||||||
// This is the Phase 0 skeleton: it only reports its version so the build
|
// Phase 1 wires those roles up as a supervised process with a minimal HTTP
|
||||||
// pipeline (ldflags stamping, docker build) can be wired up end to end.
|
// stub, a journal-milter socket stub (so the Postfix start wrapper's readiness
|
||||||
|
// probe passes) and a log-tailer stub. Real behaviour lands in later phases.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"codeberg.org/mix/selfpost/internal/buildinfo"
|
"codeberg.org/mix/selfpost/internal/buildinfo"
|
||||||
)
|
)
|
||||||
@@ -23,5 +29,81 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stdout, "selfpost panel %s (skeleton)\n", buildinfo.Version)
|
log.SetFlags(log.LstdFlags | log.LUTC)
|
||||||
|
log.SetPrefix("panel: ")
|
||||||
|
|
||||||
|
if err := run(); err != nil {
|
||||||
|
log.Fatalf("fatal: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// config holds the runtime knobs the panel reads from the environment. Every
|
||||||
|
// value has a safe default so the binary also runs outside the container.
|
||||||
|
type config struct {
|
||||||
|
httpAddr string
|
||||||
|
journalSocket string
|
||||||
|
mailLog string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() config {
|
||||||
|
return config{
|
||||||
|
httpAddr: envDefault("PANEL_HTTP_ADDR", ":8080"),
|
||||||
|
journalSocket: envDefault("JOURNAL_MILTER_SOCKET", "/run/selfpost/journal.sock"),
|
||||||
|
mailLog: envDefault("MAIL_LOG", "/var/log/mail.log"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envDefault(key, def string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// run starts the panel's three roles and blocks until a shutdown signal or the
|
||||||
|
// first fatal error from any role. A signal triggers a clean stop of all roles;
|
||||||
|
// a role error cancels the others and is returned so the process exits non-zero
|
||||||
|
// (letting supervisord/Docker see the failure — spec 4).
|
||||||
|
func run() error {
|
||||||
|
cfg := loadConfig()
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
log.Printf("starting selfpost panel %s", buildinfo.Version)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errc := make(chan error, 3)
|
||||||
|
|
||||||
|
roles := []struct {
|
||||||
|
name string
|
||||||
|
fn func(context.Context) error
|
||||||
|
}{
|
||||||
|
{"http", func(ctx context.Context) error { return serveHTTP(ctx, cfg.httpAddr) }},
|
||||||
|
{"journal-milter", func(ctx context.Context) error { return serveJournalStub(ctx, cfg.journalSocket) }},
|
||||||
|
{"log-tailer", func(ctx context.Context) error { return tailMailLog(ctx, cfg.mailLog) }},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range roles {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(name string, fn func(context.Context) error) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := fn(ctx); err != nil {
|
||||||
|
errc <- fmt.Errorf("%s: %w", name, err)
|
||||||
|
}
|
||||||
|
}(r.name, r.fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Printf("shutdown signal received, stopping")
|
||||||
|
wg.Wait()
|
||||||
|
log.Printf("panel stopped cleanly")
|
||||||
|
return nil
|
||||||
|
case err := <-errc:
|
||||||
|
log.Printf("role failed: %v", err)
|
||||||
|
stop() // cancel ctx so the other roles wind down
|
||||||
|
wg.Wait()
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user