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
101 lines
2.5 KiB
Go
101 lines
2.5 KiB
Go
package smallwebwaf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
"sneak.berlin/go/smallwebwaf/internal/proxy"
|
|
)
|
|
|
|
// healthCheckTimeout bounds the whole health check.
|
|
const healthCheckTimeout = 5 * time.Second
|
|
|
|
var errHealthEndpoint = errors.New("smallwebwaf's health endpoint answered")
|
|
|
|
// HealthCheck is the container's health check. It returns 0 while
|
|
// smallwebwaf answers its health endpoint on 127.0.0.1, at the port in
|
|
// SWWAF_LISTEN_ADDR, and the app accepts connections at the address in
|
|
// SWWAF_UPSTREAM_URL. Otherwise it writes why to stderr and returns 1.
|
|
// args are the arguments after `healthcheck`; it takes none, and given
|
|
// one it names it on stderr and returns 1 without checking anything.
|
|
func HealthCheck(
|
|
ctx context.Context, args []string, lookupEnv func(string) (string, bool),
|
|
stderr io.Writer,
|
|
) int {
|
|
if len(args) > 0 {
|
|
_, _ = fmt.Fprintf(stderr,
|
|
"smallwebwaf healthcheck: unexpected argument %q\n", args[0])
|
|
|
|
return 1
|
|
}
|
|
|
|
err := healthCheck(ctx, lookupEnv)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintln(stderr, "unhealthy:", err)
|
|
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func healthCheck(ctx context.Context, lookupEnv func(string) (string, bool)) error {
|
|
ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
|
|
defer cancel()
|
|
|
|
cfg, err := config.FromEnvironment(lookupEnv)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid setting: %w", err)
|
|
}
|
|
|
|
// The settings have checked that the address has a port.
|
|
_, port, _ := net.SplitHostPort(cfg.ListenAddr)
|
|
health := "http://" + net.JoinHostPort("127.0.0.1", port) + proxy.HealthPath
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, health, http.NoBody)
|
|
if err != nil {
|
|
return fmt.Errorf("make the request: %w", err)
|
|
}
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("ask smallwebwaf: %w", err)
|
|
}
|
|
|
|
_ = res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("%w %s", errHealthEndpoint, res.Status)
|
|
}
|
|
|
|
conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", appAddress(cfg.UpstreamURL))
|
|
if err != nil {
|
|
return fmt.Errorf("connect to the app: %w", err)
|
|
}
|
|
|
|
_ = conn.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
// appAddress is the host and port of the app's URL, the port being the
|
|
// scheme's own when the URL names none.
|
|
func appAddress(app *url.URL) string {
|
|
port := app.Port()
|
|
if port == "" {
|
|
port = "80"
|
|
if app.Scheme == "https" {
|
|
port = "443"
|
|
}
|
|
}
|
|
|
|
return net.JoinHostPort(app.Hostname(), port)
|
|
}
|