diff --git a/TODO.md b/TODO.md index a8c1513..8996811 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,13 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of # Completed Steps +- 2026-10-19: `bin/entrypoint.sh` has each of its steps in a function, and a + `main` at the end calls them in order, as the shell section of the code style + guide asks ([#163](https://git.eeqj.de/sneak/netwatch/issues/163)). It is + still POSIX sh with no `set -e`, and does what it did, with the same messages + and exit codes. Checked by running the image with each script: a normal start, + bad `PORT`, `TRUSTED_PROXIES` and `DATA_DIR` values, `docker stop`, INT, and + nginx exiting on its own - 2026-10-19: while report files cannot be written, the health check answers 503 with `"status":"error"` and the same fields, so the image's `HEALTHCHECK` reports the container `unhealthy` diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh index db7c3ff..7bae9d4 100755 --- a/bin/entrypoint.sh +++ b/bin/entrypoint.sh @@ -13,24 +13,27 @@ set -u # address and start anyway, and reports a bad port without naming # PORT, so a value that is not a usable port stops the container here, # before either process starts. -export PORT="${PORT:-8080}" -case "$PORT" in - *[!0-9]*) - echo "entrypoint: PORT must be a port number, not '$PORT'" >&2 +check_port() { + export PORT="${PORT:-8080}" + case "$PORT" in + *[!0-9]*) + echo "entrypoint: PORT must be a port number, not '$PORT'" >&2 + exit 1 + ;; + esac + # The length is checked first because, for a number too big for it, + # the shell's test prints an error and is false, so the range checks + # alone would let it through. + if [ "${#PORT}" -gt 5 ] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then + echo "entrypoint: PORT must be from 1 to 65535, not '$PORT'" >&2 exit 1 - ;; -esac -# The length is checked first because, for a number too big for it, -# the shell's test prints an error and is false, so the range checks -# alone would let it through. -if [ "${#PORT}" -gt 5 ] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then - echo "entrypoint: PORT must be from 1 to 65535, not '$PORT'" >&2 - exit 1 -fi -if [ "$PORT" -eq 8081 ]; then - echo "entrypoint: PORT cannot be 8081, netwatch-server listens there" >&2 - exit 1 -fi + fi + if [ "$PORT" -eq 8081 ]; then + echo "entrypoint: PORT cannot be 8081, netwatch-server" \ + "listens there" >&2 + exit 1 + fi +} # TRUSTED_PROXIES names the reverse proxies in front of the container, # as IP addresses or CIDRs separated by commas. nginx takes the client @@ -47,21 +50,23 @@ fi # TRUSTED_PROXIES. Its error, naming the CIDR, is dropped for the one # below, naming the entry as written. set -f keeps a * in an entry from # becoming a list of file names. -TRUSTED_PROXIES="${TRUSTED_PROXIES-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}" -set -f -for proxy in $(printf '%s' "$TRUSTED_PROXIES" | tr ',' ' '); do - case "$proxy" in - */*) cidr="$proxy" ;; - *:*) cidr="$proxy/128" ;; - *) cidr="$proxy/32" ;; - esac - if ! netwatch-server check-cidr "$cidr" 2> /dev/null; then - echo "entrypoint: TRUSTED_PROXIES must be IP addresses or CIDRs" \ - "separated by commas; '$proxy' is neither" >&2 - exit 1 - fi - echo "set_real_ip_from $cidr;" -done > /etc/nginx/trusted-proxies.conf +write_trusted_proxies() { + TRUSTED_PROXIES="${TRUSTED_PROXIES-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}" + set -f + for proxy in $(printf '%s' "$TRUSTED_PROXIES" | tr ',' ' '); do + case "$proxy" in + */*) cidr="$proxy" ;; + *:*) cidr="$proxy/128" ;; + *) cidr="$proxy/32" ;; + esac + if ! netwatch-server check-cidr "$cidr" 2> /dev/null; then + echo "entrypoint: TRUSTED_PROXIES must be IP addresses or CIDRs" \ + "separated by commas; '$proxy' is neither" >&2 + exit 1 + fi + echo "set_real_ip_from $cidr;" + done > /etc/nginx/trusted-proxies.conf +} # netwatch-server keeps its report files in DATA_DIR, on the /data # volume, which may be a host directory owned by root or by another @@ -73,12 +78,10 @@ done > /etc/nginx/trusted-proxies.conf # naming DATA_DIR, unless DATA_DIR is /data or a path below it, and it # acts on nothing outside /data, whatever symbolic or hard links it # meets there. -export DATA_DIR="${DATA_DIR:-/data/reports}" -netwatch-server prepare-data-dir "$DATA_DIR" || exit 1 - -# A stop signal is only noted here; the loop below acts on it. -stop_requested="" -trap 'stop_requested=yes' TERM INT +prepare_data_dir() { + export DATA_DIR="${DATA_DIR:-/data/reports}" + netwatch-server prepare-data-dir "$DATA_DIR" || exit 1 +} # netwatch-server runs as the netwatch user and listens on loopback # only, on a port other than the public one; configs/nginx.conf proxies @@ -87,9 +90,11 @@ trap 'stop_requested=yes' TERM INT # the container has. The netwatch user has no login shell, hence -s # /bin/sh. busybox su replaces itself with the command instead of # staying on as its parent, so $! is the server's own PID. -BIND_ADDRESS=127.0.0.1 PORT=8081 TRUSTED_PROXIES=127.0.0.1/32 \ - su -s /bin/sh netwatch -c 'exec netwatch-server' & -backend=$! +start_backend() { + BIND_ADDRESS=127.0.0.1 PORT=8081 TRUSTED_PROXIES=127.0.0.1/32 \ + su -s /bin/sh netwatch -c 'exec netwatch-server' & + backend=$! +} # nginx starts through the nginx image's own entrypoint, which applies # the image's start-up configuration and then replaces itself with @@ -98,9 +103,11 @@ backend=$! # that rendering to PORT: a variable nginx itself uses, such as $uri, # would otherwise be replaced by an environment variable of the same # name. -NGINX_ENVSUBST_FILTER='^PORT$' \ - /docker-entrypoint.sh nginx -g 'daemon off;' & -nginx=$! +start_nginx() { + NGINX_ENVSUBST_FILTER='^PORT$' \ + /docker-entrypoint.sh nginx -g 'daemon off;' & + nginx=$! +} running() { kill -0 "$1" 2>/dev/null @@ -109,26 +116,47 @@ running() { # POSIX sh cannot wait for whichever of two children exits first, so # look once a second. The shell collects a child that has exited while # it runs sleep, and running() is false for that child from then on. -while [ -z "$stop_requested" ] && running "$backend" && running "$nginx"; do - sleep 1 -done +wait_for_stop_or_exit() { + while [ -z "$stop_requested" ] && running "$backend" && running "$nginx"; do + sleep 1 + done +} # Stop both, then wait until neither is left. -kill -TERM "$backend" "$nginx" 2>/dev/null -while running "$backend" || running "$nginx"; do - sleep 1 -done - -wait "$backend" -backend_status=$? -wait "$nginx" -nginx_status=$? -echo "entrypoint: netwatch-server exited $backend_status," \ - "nginx exited $nginx_status" +stop_both() { + kill -TERM "$backend" "$nginx" 2>/dev/null + while running "$backend" || running "$nginx"; do + sleep 1 + done +} # Success is a requested stop that both processes exited cleanly from. -if [ -n "$stop_requested" ] && [ "$backend_status" -eq 0 ] && - [ "$nginx_status" -eq 0 ]; then - exit 0 -fi -exit 1 +exit_with_status() { + wait "$backend" + backend_status=$? + wait "$nginx" + nginx_status=$? + echo "entrypoint: netwatch-server exited $backend_status," \ + "nginx exited $nginx_status" + if [ -n "$stop_requested" ] && [ "$backend_status" -eq 0 ] && + [ "$nginx_status" -eq 0 ]; then + exit 0 + fi + exit 1 +} + +main() { + check_port + write_trusted_proxies + prepare_data_dir + # A stop signal is only noted here; wait_for_stop_or_exit acts on it. + stop_requested="" + trap 'stop_requested=yes' TERM INT + start_backend + start_nginx + wait_for_stop_or_exit + stop_both + exit_with_status +} + +main "$@"