check / check (push) Failing after 3s
The Dockerfile's last stage is now the image of "Deployment" in SPEC.md: Ubuntu 26.04 with ca-certificates, nix-bin and runit from a dated snapshot whose InRelease files are checked by hash, nixpkgs from its release file checked by SHA-256, runsvinit built at a fixed commit, and smallwebwaf as a runit service. smallwebwaf answers /_smallwebwaf/healthz, and `smallwebwaf healthcheck`, which takes no further argument, is the image's HEALTHCHECK. script/example-app builds an app on the image and checks it end to end. The Nix profile comes last on the PATH: first, busybox from nixpkgs replaced runit's own runsvdir and sv. SPEC.md is corrected to match what was built. Model: opus-5-5
138 lines
3.4 KiB
Go
138 lines
3.4 KiB
Go
// Package smallwebwaf runs the smallwebwaf process: it reads the settings,
|
|
// serves requests until it is told to stop, and then stops in an orderly
|
|
// way.
|
|
package smallwebwaf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
|
"sneak.berlin/go/smallwebwaf/internal/proxy"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
// shutdownTimeout is how long requests in progress may take to finish
|
|
// once smallwebwaf is told to stop, before their connections are closed.
|
|
// runit and docker wait a little longer before they kill the process.
|
|
const shutdownTimeout = 5 * time.Second
|
|
|
|
// Params are what Run needs from the process.
|
|
type Params struct {
|
|
// Version is the version of the binary, set when it is built.
|
|
Version string
|
|
// LookupEnv reads an environment variable, normally os.LookupEnv.
|
|
LookupEnv func(string) (string, bool)
|
|
// Stdout receives the request log and the process's own messages.
|
|
Stdout io.Writer
|
|
}
|
|
|
|
// Main runs smallwebwaf until SIGTERM or SIGINT, and returns the
|
|
// process's exit status. Run as `smallwebwaf healthcheck`, it is the
|
|
// container's health check instead.
|
|
func Main(version string) int {
|
|
if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
|
|
return HealthCheck(context.Background(), os.Args[2:], os.LookupEnv, os.Stderr)
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(),
|
|
syscall.SIGTERM, os.Interrupt)
|
|
defer stop()
|
|
|
|
return Run(ctx, Params{
|
|
Version: version,
|
|
LookupEnv: os.LookupEnv,
|
|
Stdout: os.Stdout,
|
|
})
|
|
}
|
|
|
|
// Run reads the settings, then serves requests until ctx is done. It
|
|
// returns the process's exit status, 1 when smallwebwaf cannot start.
|
|
func Run(ctx context.Context, params Params) int {
|
|
processLog := requestlog.NewProcessLogger(params.Stdout)
|
|
|
|
cfg, err := config.FromEnvironment(params.LookupEnv)
|
|
if err != nil {
|
|
processLog.Error("invalid setting", "error", err.Error())
|
|
|
|
return 1
|
|
}
|
|
|
|
listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", cfg.ListenAddr)
|
|
if err != nil {
|
|
processLog.Error("cannot listen on SWWAF_LISTEN_ADDR",
|
|
"error", err.Error())
|
|
|
|
return 1
|
|
}
|
|
|
|
server := proxy.New(proxy.Params{
|
|
Config: cfg,
|
|
RequestLog: params.Stdout,
|
|
ProcessLog: processLog,
|
|
GeoJSURL: lookup.URL,
|
|
})
|
|
|
|
processLog.Info("starting",
|
|
"version", params.Version,
|
|
"address", listener.Addr().String(),
|
|
"settings", cfg)
|
|
|
|
return serve(ctx, server, listener, processLog)
|
|
}
|
|
|
|
// serve serves requests on listener until ctx is done, then gives the
|
|
// requests in progress shutdownTimeout to finish.
|
|
func serve(
|
|
ctx context.Context, server *http.Server, listener net.Listener,
|
|
processLog *slog.Logger,
|
|
) int {
|
|
served := make(chan error, 1)
|
|
|
|
go func() {
|
|
served <- server.Serve(listener)
|
|
}()
|
|
|
|
select {
|
|
case err := <-served:
|
|
processLog.Error("serving failed", "error", err.Error())
|
|
|
|
return 1
|
|
case <-ctx.Done():
|
|
}
|
|
|
|
processLog.Info("stopping")
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx),
|
|
shutdownTimeout)
|
|
defer cancel()
|
|
|
|
err := server.Shutdown(shutdownCtx)
|
|
if err != nil {
|
|
processLog.Warn("requests still in progress were cut off",
|
|
"error", err.Error())
|
|
|
|
_ = server.Close()
|
|
}
|
|
|
|
err = <-served
|
|
if !errors.Is(err, http.ErrServerClosed) {
|
|
processLog.Error("serving failed", "error", err.Error())
|
|
|
|
return 1
|
|
}
|
|
|
|
processLog.Info("stopped")
|
|
|
|
return 0
|
|
}
|