Files
cloud-apache-container/scripts/entrypoint-shared-ols.sh
T
shadowdaoandClaude Opus 5 8790b027a9 fix(lsphp): stop SIGPIPE+pipefail reporting the parity extension as missing
`entrypoint-lsphp.sh` decided whether cac_path_parity was loaded with

    printf '%s\n' "$LSPHP_INFO" | grep -q '^cac_path_parity support => enabled$'

under `set -euo pipefail`. `grep -q` exits on its first match; printf is still
writing the remaining ~40 KB of `lsphp -i`, takes SIGPIPE, exits 141, and
pipefail prefers 141 over grep's 0. The branch therefore evaluated FALSE
*because the extension was present* — present early enough to stop the reader —
and every affected container fell back to the auto_prepend normaliser that a
customer's own .user.ini silently displaces, i.e. the exact failure the
extension exists to remove. Measured on whp02 against the published
cac-lsphp:php83: 5/5 runs status=141 with pipefail, 0 without.

The race is decided by pipe capacity, which is why it reproduced on whp02 and
not on other daemons: while the payload fits the pipe the writer never blocks
and always finishes first. Forced over the limit it is deterministic — 3x the
same `lsphp -i` (122100 bytes) gives 141 every time in the built image.

Fixed by reading with here-strings, which are not pipelines at all, so there is
no second exit status for pipefail to adopt. Same grep/awk patterns; plumbing
only. Same class fixed everywhere it existed under pipefail:

  * entrypoint-lsphp.sh      parity probe, and the SCAN_DIR awk probe
  * entrypoint-litespeed.sh  SCAN_DIR probe (a bare assignment: 141 there does
                             not degrade, `set -e` kills PID 1), and ols_running
  * entrypoint-shared-ols.sh ols_running
  * render-shared-ols-config.sh  site.meta parsing (`sed | head -1`): measured
                             141 at 6000 duplicate keys, which under `set -e`
                             aborts the whole render
  * fpm-parity-check.sh      the `php-fpm -m` pre-flight, whose whole job is to
                             stop a harness fault being blamed on the extension

Also: the fallback used to announce "cac_path_parity extension not loadable in
this image" for every reason the branch was reached, including its own plumbing
breaking — a false diagnosis that sends operators to rebuild a good image whose
build gate passed. Verdicts now carry the evidence they rest on, and a probe
that produced nothing is reported as a probe failure that establishes nothing
about the image. Fail-open posture is unchanged: no probe failure is fatal.

Adds scripts/tests/lsphp-info-probe.test.sh, which runs the shipped probes
(extracted verbatim, so they cannot drift from what runs in production) under
`set -euo pipefail` against a realistic ~40 KB phpinfo body, and statically
outlaws the shape repo-wide. Against trunk it fails, naming all 9 offending
lines. Wired into CI as a new Shell-Checks job, because no existing gate ever
executed the entrypoint's branch logic — the .phpt suite and the Dockerfile's
own `lsphp -i | grep -q` probe (which has no pipefail) were both green for the
release whose entrypoint declared that same extension missing.

Verified: PHP 8.3 --no-cache build green, 10/10 .phpt, 9/9 FPM harness; the
built image logs `path parity = extension` and reports `Rewriting => active`
with .from/.to populated; ext-removed and probe-broken variants each produce
their own honest message and still start.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:23:56 -07:00

136 lines
4.9 KiB
Bash

#!/usr/bin/env bash
## entrypoint-shared-ols.sh — PID 1 for the shared-ols tier.
##
## One OpenLiteSpeed container fronting MANY tenants' detached cac-lsphp
## sidecars (the OLS analogue of the shared-httpd container). Webserver ONLY —
## it runs NO PHP locally (render-shared-ols-config.sh strips the stock local
## lsphp; every site's PHP goes to its own sidecar over LSAPI). HAProxy stays
## the TLS/WAF/SNI edge and routes OLS-type hostnames here on :443.
##
## Reuses cac-litespeed's hard-won DAEMON-MODE supervision (NOT `openlitespeed
## -n` + wait): OLS self-restarts on QUIC.cloud IP refresh would otherwise exit
## PID 1 cleanly and tear the container down. See entrypoint-litespeed.sh and
## feedback_ols_quiccloud_restart_kills_container.
set -euo pipefail
: "${environment:=PROD}"
export CONTAINER_ROLE="shared_ols"
LSWS_CONF=/usr/local/lsws/conf
CERT_DIR="$LSWS_CONF/cert"
HEALTH_DIR=/usr/local/lsws/shared-ols-health
export SITES_ROOT="${SITES_ROOT:-$LSWS_CONF/shared-sites}"
export LSCACHE_ROOT="${LSCACHE_ROOT:-/var/lscache}"
export CERT_FILE="$CERT_DIR/shared-ols.crt"
export KEY_FILE="$CERT_DIR/shared-ols.key"
mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT" "$CERT_DIR" "$HEALTH_DIR/html"
## ---- self-signed cert for the :443 listener (HAProxy verifies none) ----
if [ ! -f "$CERT_FILE" ]; then
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout "$KEY_FILE" -out "$CERT_FILE" -subj "/CN=shared-ols" 2>/dev/null
fi
## ---- health vhost (catch-all): valid server with zero customer sites +
## answers HAProxy health checks that hit by IP / unknown Host with a 200 ----
cat > "$HEALTH_DIR/vhconf.conf" <<'EOF'
docRoot $VH_ROOT/html
enableScript 0
context / {
allowBrowse 1
location $DOC_ROOT/
}
EOF
printf 'ok\n' > "$HEALTH_DIR/html/healthz"
printf 'shared-ols\n' > "$HEALTH_DIR/html/index.html"
## ---- ownership: OLS reads conf/ as lsadm. chown the base conf dir + health dir
## NON-recursively (the per-site files under conf/shared-sites are written by the
## panel and are world-readable; a recursive chown here would be O(N-sites) on
## every container (re)start, delaying first-listen after a crash). The render
## script chowns the httpd_config.conf it produces. ----
chown lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" "$HEALTH_DIR/html" 2>/dev/null || true
chown lsadm:nogroup "$HEALTH_DIR/vhconf.conf" "$HEALTH_DIR/html/healthz" "$HEALTH_DIR/html/index.html" 2>/dev/null || true
## ---- assemble httpd_config.conf from the panel's per-site files ----
/scripts/render-shared-ols-config.sh
## ---- stream OLS logs to PID-1 stdout (follows across restarts) ----
mkdir -p /usr/local/lsws/logs
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
tail -F /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log 2>/dev/null &
## ---- .htaccess watcher (required; spec 5.3). Background; the panel monitors
## that it stays alive (its death silently stops rewrite changes applying). ----
/scripts/ols-htaccess-watcher.sh &
WATCHER_PID=$!
## ---- supervise OLS in DAEMON mode (verbatim model from entrypoint-litespeed.sh) ----
STOP_REQUESTED=0
term_handler() {
STOP_REQUESTED=1
kill "$WATCHER_PID" 2>/dev/null || true
/usr/local/lsws/bin/lswsctrl stop >/dev/null 2>&1 || true
}
trap term_handler TERM INT
## Variable + here-string, not a pipe into `grep -qi` — see the long note on the
## identical function in entrypoint-litespeed.sh: `grep -q` closing the pipe on
## a match can leave the writer dying 141, and `set -o pipefail` (line 14) turns
## that into "OLS is down" *because* the running line matched. A non-zero
## `lswsctrl` still counts as not running, as pipefail made it count before.
ols_running() {
local st
st=$(/usr/local/lsws/bin/lswsctrl status 2>/dev/null) || return 1
grep -qi 'running with pid' <<<"$st"
}
MAX_STARTS=5
WINDOW=60
starts=""
start_ols() {
/usr/local/lsws/bin/lswsctrl start >/dev/null 2>&1 || true
for _ in $(seq 1 20); do
ols_running && return 0
sleep 0.5
done
return 1
}
if ! start_ols; then
echo "entrypoint-shared-ols: OLS failed to start (not running after 10s)." >&2
exit 1
fi
echo "entrypoint-shared-ols: OLS started in daemon mode — $(/usr/local/lsws/bin/lswsctrl status 2>/dev/null || true)"
while true; do
if ols_running; then
sleep 3
continue
fi
sleep 2
if [ "$STOP_REQUESTED" -eq 0 ] && ols_running; then
continue
fi
if [ "$STOP_REQUESTED" -eq 1 ]; then
echo "entrypoint-shared-ols: SIGTERM received, OLS stopped — exiting."
exit 0
fi
now=$(date +%s)
starts="$starts $now"
pruned=""
for t in $starts; do
[ $((now - t)) -lt "$WINDOW" ] && pruned="$pruned $t"
done
starts="$pruned"
n=$(echo $starts | wc -w)
echo "entrypoint-shared-ols: OLS not running — relaunching (attempt $n/$MAX_STARTS within ${WINDOW}s)." >&2
if [ "$n" -ge "$MAX_STARTS" ]; then
echo "entrypoint-shared-ols: OLS crash-looping — bailing for Docker restart policy / monitor." >&2
exit 1
fi
start_ols || true
done