From 19db8f170a3447e096d989984032a7366a3d63db Mon Sep 17 00:00:00 2001 From: jknapp Date: Wed, 10 Jun 2026 01:22:14 -0700 Subject: [PATCH] feat(shared-ols): shared OpenLiteSpeed tier image (webserver-only, fronts cac-lsphp sidecars) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One OLS container fronting many tenants' detached cac-lsphp sidecars — the OLS analogue of shared-httpd. Runs NO PHP locally; every site's PHP goes to its own sidecar over LSAPI (extProcessor type lsapi, address :9000). Key design fact (established by PoC): OLS has NO top-level 'include' directive, so render-shared-ols-config.sh assembles httpd_config.conf from the panel's per-site files (vhconf.conf + site.meta) at boot and on every change — the 'include' OLS lacks. Per-site detail uses the OLS-native configFile + vhost-scoped extprocessor model. LSCache is module-level (a configFile-loaded vhost rejects a bare cache{} block); the WP LiteSpeed plugin controls cacheability via X-LiteSpeed-Cache-Control headers. - Dockerfile.shared-ols: litespeed base + inotify-tools/envsubst/openssl, admin bound to loopback, :80/:443 self-signed, healthz HEALTHCHECK. - entrypoint-shared-ols.sh: cert + health vhost + render + watcher, then daemon-mode OLS supervision (reused from cac-litespeed so self-restarts don't kill PID 1). - render-shared-ols-config.sh: strip stock (incl local lsphp) + append base + per-site stanzas + listeners with all maps + catch-all health vhost. - ols-htaccess-watcher.sh: inotify debounce+floor -> lswsctrl restart (spec 5.3). - configs/shared-ols/{httpd_config_base,vhconf}.tpl. - CI: Build-Shared-OLS job. Verified locally end-to-end: zero-site boot healthy on :443; add site via the panel contract -> Host-routed to the right sidecar (SAPI=litespeed); real client IP + HTTPS behind X-Forwarded headers; LSCache miss->hit; .htaccess change triggers graceful restart; unknown Host hits health catch-all (200). Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/build-push.yaml | 33 ++++++ Dockerfile.shared-ols | 57 +++++++++++ configs/shared-ols/httpd_config_base.tpl | 38 +++++++ configs/shared-ols/vhconf.tpl | 70 +++++++++++++ scripts/entrypoint-shared-ols.sh | 120 ++++++++++++++++++++++ scripts/ols-htaccess-watcher.sh | 58 +++++++++++ scripts/render-shared-ols-config.sh | 125 +++++++++++++++++++++++ 7 files changed, 501 insertions(+) create mode 100644 Dockerfile.shared-ols create mode 100644 configs/shared-ols/httpd_config_base.tpl create mode 100644 configs/shared-ols/vhconf.tpl create mode 100644 scripts/entrypoint-shared-ols.sh create mode 100644 scripts/ols-htaccess-watcher.sh create mode 100644 scripts/render-shared-ols-config.sh diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 4a85576..b290d1a 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -185,3 +185,36 @@ jobs: push: true tags: | repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest + + Build-Shared-OLS: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea + uses: docker/login-action@v3 + with: + registry: repo.anhonesthost.net + username: ${{ secrets.CI_USER }} + password: ${{ secrets.CI_TOKEN }} + + - name: Build and Push Shared OLS Image + uses: docker/build-push-action@v6 + with: + file: ./Dockerfile.shared-ols + platforms: linux/amd64 + push: true + # Single image (runs no PHP). PHPVER just selects the OLS base tag; + # pinned to 83 / OLS 1.8.4 to match the rest of the litespeed family. + build-args: | + PHPVER=83 + OLS_VERSION=1.8.4 + tags: | + repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest diff --git a/Dockerfile.shared-ols b/Dockerfile.shared-ols new file mode 100644 index 0000000..074497d --- /dev/null +++ b/Dockerfile.shared-ols @@ -0,0 +1,57 @@ +## shared-ols — the shared OpenLiteSpeed webserver tier. +## +## One OLS container fronting MANY tenants' detached cac-lsphp sidecars — the +## OLS analogue of the shared-httpd container. Runs NO PHP locally: every site's +## PHP goes to its own cac-lsphp:phpNN sidecar over LSAPI (extProcessor type +## lsapi, address :9000). HAProxy stays the TLS/WAF/SNI edge and routes +## OLS-type hostnames here on :443. +## +## Built on the SAME litespeedtech prebuilt base as cac-litespeed / cac-lsphp so +## the OLS build + plumbing (lscgid, cgid socket — see feedback_ols_packaging_landmines) +## are the proven ones. The base is lsphp-tagged but we never run that lsphp; +## the tag just selects the OLS build. Pinned to lsphp83 / OLS 1.8.4. +## +## Config model (established by PoC 2026-06-10): OLS has NO top-level `include`, +## so render-shared-ols-config.sh assembles httpd_config.conf from the panel's +## per-site files at boot + on every change. See that script + the plan. + +ARG OLS_VERSION=1.8.4 +ARG PHPVER=83 +FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} + +## Tooling the shared tier needs on top of the base: +## - inotify-tools: the .htaccess watcher (spec 5.3) +## - gettext-base: envsubst for render-shared-ols-config.sh +## - openssl: self-signed cert for the :443 listener (HAProxy verifies none) +## - curl/ca-certificates: HEALTHCHECK +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + inotify-tools gettext-base openssl ca-certificates curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* + +## Snapshot the stock httpd_config.conf so render-shared-ols-config.sh always has +## a pristine base to strip-and-rebuild from (the base image keeps it at conf/). +RUN mkdir -p /usr/local/lsws/.conf && \ + cp /usr/local/lsws/conf/httpd_config.conf /usr/local/lsws/.conf/httpd_config.conf + +COPY ./scripts/entrypoint-shared-ols.sh \ + ./scripts/render-shared-ols-config.sh \ + ./scripts/ols-htaccess-watcher.sh \ + /scripts/ +RUN chmod +x /scripts/entrypoint-shared-ols.sh /scripts/render-shared-ols-config.sh /scripts/ols-htaccess-watcher.sh +COPY ./configs/shared-ols/ /etc/shared-ols-templates/ + +## Admin console unreachable from tenant/edge networks (spec 5.2): bind the +## WebAdmin listener to loopback. Same sed as Dockerfile.litespeed. +RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0.0.1:|' \ + /usr/local/lsws/admin/conf/admin_config.conf 2>/dev/null || true + +EXPOSE 80 443 + +## Health: the entrypoint renders a catch-all _health vhost serving /healthz, so +## this passes from boot (zero customer sites) onward. Self-signed :443. +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD curl -fsSk https://127.0.0.1/healthz || exit 1 + +ENTRYPOINT ["/scripts/entrypoint-shared-ols.sh"] diff --git a/configs/shared-ols/httpd_config_base.tpl b/configs/shared-ols/httpd_config_base.tpl new file mode 100644 index 0000000..3cb41ec --- /dev/null +++ b/configs/shared-ols/httpd_config_base.tpl @@ -0,0 +1,38 @@ +## ---- shared-ols append (do not edit below) ---- +## Server-level config for the SHARED OpenLiteSpeed tier. Appended to the +## stock httpd_config.conf AFTER render-shared-ols-config.sh strips the stock +## listeners, vhTemplate docker, AND the stock `extProcessor lsphp` + +## `scriptHandler` (so this server NEVER runs PHP locally — every site's PHP +## goes to its own detached cac-lsphp sidecar over LSAPI). Rendered with +## envsubst; only ${LSCACHE_ROOT} is substituted here. + +serverName shared-ols + +## Real client IP behind HAProxy. HAProxy sets X-Forwarded-For (the real +## client) and X-Forwarded-Proto. Mode 2 = trust the proxy header. HAProxy is +## the only thing that ever connects to this tier (it's not publicly exposed), +## so trusting the header from the docker-network peer is safe — same trust +## model as the shared httpd's RemoteIPInternalProxy. +useIpInProxyHeader 2 + +## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume, +## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath). +## enableCache/enablePrivateCache ON here means the cache module is ACTIVE, but a +## response is only cached if it's marked cacheable — the LiteSpeed Cache WP +## plugin sets X-LiteSpeed-Cache-Control headers, and checkPublic/PrivateCache + +## ignoreRespCacheCtrl=0 make OLS honor them. No plugin → nothing cached (safe). +module cache { + storagePath ${LSCACHE_ROOT} + checkPrivateCache 1 + checkPublicCache 1 + maxCacheObjSize 10000000 + maxStaleAge 200 + qsCache 1 + reqCookieCache 1 + respCookieCache 1 + ignoreReqCacheCtrl 0 + ignoreRespCacheCtrl 0 + enableCache 1 + enablePrivateCache 1 +} +## ---- end shared-ols server append ---- diff --git a/configs/shared-ols/vhconf.tpl b/configs/shared-ols/vhconf.tpl new file mode 100644 index 0000000..8add16c --- /dev/null +++ b/configs/shared-ols/vhconf.tpl @@ -0,0 +1,70 @@ +## Per-site OLS vhost detail — rendered by the WHP panel (shared_ols_manager) +## to $SITES_ROOT//vhconf.conf and referenced from the vhost stanza's +## `configFile` in httpd_config.conf. ~~PLACEHOLDERS~~ are filled by the panel +## (matches the shared-vhost-template.tpl convention). One directive per line — +## OLS PlainConf does NOT accept ';' separators. +## +## CRITICAL (feedback_ols_lsapi_no_script_filename_remap): docRoot here MUST be +## the SAME absolute path the cac-lsphp sidecar has mounted, because OLS hands +## lsphp exactly docRoot+URI as SCRIPT_FILENAME and lsphp opens it. Both are +## /mnt/users///public_html. The panel asserts this parity. + +docRoot ~~DOCROOT~~ +enableScript 1 + +## Remote detached lsphp over LSAPI/TCP. address = the site's sidecar container +## on the docker network. autoStart 0 = OLS NEVER spawns it (it's a separate +## container). maxConns MUST equal the sidecar's PHP_LSAPI_CHILDREN — the panel +## writes both from the single fpm_max_children value so they can't drift. +## NO `env` lines: detached lsphp owns its env in the sidecar (spec 5.2). +## NOTE on `path`: required syntactically but UNUSED for a remote autoStart-0 +## processor (OLS never spawns it). Point it at a path that always exists in the +## shared-ols image (the stock fcgi-bin/lsphp), NOT a version-specific +## /usr/local/lsws/lsphpNN — the shared-ols image carries only one lsphp build, +## while sites may run any PHP version on their sidecar. The sidecar owns the +## real PHP runtime/version. +extprocessor ~~VHNAME~~_lsphp { + type lsapi + address ~~SIDECAR~~:9000 + maxConns ~~MAXCONNS~~ + autoStart 0 + path /usr/local/lsws/fcgi-bin/lsphp + initTimeout 60 + retryTimeout 0 + respBuffer 0 + persistConn 1 +} + +scripthandler { + add lsapi:~~VHNAME~~_lsphp php +} + +## context / drives static serving + .htaccess. RewriteFile .htaccess is OLS's +## autoLoadHtaccess equivalent — re-read on graceful restart (the watcher +## triggers that within the documented window). +context / { + allowBrowse 1 + location $DOC_ROOT/ + rewrite { + enable 1 + RewriteFile .htaccess + } + addDefaultCharset off +} + +## LSCache is enabled at MODULE scope (httpd_config_base.tpl) and honored per +## response via the LiteSpeed Cache WP plugin's X-LiteSpeed-Cache-Control +## headers — a `configFile`-loaded vhost in OLS 1.8.4 does NOT accept a bare +## `cache {}` block (verified 2026-06-10), so there is intentionally no per-vhost +## cache block here. OLS stores each vhost's cache in its own subdir under the +## module storagePath automatically (per-vhost isolation, spec 5.2). + +errorlog ~~LOG_DIR~~/error_log { + logLevel WARN + rollingSize 50M + keepDays 7 +} +accesslog ~~LOG_DIR~~/access_log { + rollingSize 50M + keepDays 7 +} diff --git a/scripts/entrypoint-shared-ols.sh b/scripts/entrypoint-shared-ols.sh new file mode 100644 index 0000000..7fc116a --- /dev/null +++ b/scripts/entrypoint-shared-ols.sh @@ -0,0 +1,120 @@ +#!/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" + +## ---- assemble httpd_config.conf from the panel's per-site files ---- +/scripts/render-shared-ols-config.sh + +chown -R lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" 2>/dev/null || true + +## ---- 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 + +ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; } + +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 diff --git a/scripts/ols-htaccess-watcher.sh b/scripts/ols-htaccess-watcher.sh new file mode 100644 index 0000000..38e132c --- /dev/null +++ b/scripts/ols-htaccess-watcher.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +## ols-htaccess-watcher.sh — graceful-restart the shared OLS when any tenant's +## .htaccess changes. OLS reads .htaccess (RewriteFile) at (re)start, NOT per +## request, so without this a WordPress permalink/LiteSpeed-Cache change would +## silently not take effect. Required by spec 5.3. +## +## Watches all docroots for .htaccess writes, COALESCES bursts (a WP plugin save +## touches the file several times) within a debounce window, and RATE-LIMITS to +## a floor (one restart per FLOOR seconds) so many tenants saving at once can't +## trigger a restart storm. Debounce/floor are env-tunable (panel discloses the +## resulting "~60s" window to customers). +## +## Failure of THIS process is the silent-ticket failure mode (spec 7): if it +## dies, tenants' rewrite changes stop applying with no error. The entrypoint +## runs it and the panel monitors it (check-ols-htaccess-watcher.php). +set -uo pipefail + +WATCH_ROOT="${OLS_WATCH_ROOT:-/mnt/users}" +DEBOUNCE="${OLS_HTACCESS_DEBOUNCE:-15}" # coalesce window (s) +FLOOR="${OLS_HTACCESS_FLOOR:-60}" # min seconds between restarts +LSWSCTRL=/usr/local/lsws/bin/lswsctrl +last_restart=0 + +log() { echo "ols-htaccess-watcher: $*" >&2; } + +do_restart() { + now=$(date +%s) + if [ $((now - last_restart)) -lt "$FLOOR" ]; then + log "within ${FLOOR}s floor — coalescing, skipping restart" + return + fi + if "$LSWSCTRL" restart >/dev/null 2>&1; then + last_restart=$now + log "graceful restart issued (.htaccess change)" + else + log "WARNING: lswsctrl restart failed" + fi +} + +if ! command -v inotifywait >/dev/null 2>&1; then + log "FATAL: inotifywait not installed (inotify-tools)"; exit 1 +fi +mkdir -p "$WATCH_ROOT" +log "watching $WATCH_ROOT for .htaccess changes (debounce=${DEBOUNCE}s floor=${FLOOR}s)" + +## -m monitor, -r recursive. We filter to .htaccess in the read loop rather than +## --include so this works on older inotify-tools too. modify/create/delete/move +## all matter (delete of .htaccess also changes rewrite behavior). +inotifywait -m -r -e modify,create,delete,move "$WATCH_ROOT" --format '%f' 2>/dev/null | +while read -r fname; do + case "$fname" in + .htaccess) ;; + *) continue ;; + esac + ## Drain further events for DEBOUNCE seconds (coalesce the burst), then act. + while read -r -t "$DEBOUNCE" _; do :; done + do_restart +done diff --git a/scripts/render-shared-ols-config.sh b/scripts/render-shared-ols-config.sh new file mode 100644 index 0000000..eae7da7 --- /dev/null +++ b/scripts/render-shared-ols-config.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +## render-shared-ols-config.sh — assemble httpd_config.conf for the shared-ols +## tier from the per-site files the WHP panel drops into $SITES_ROOT. +## +## WHY THIS EXISTS: OpenLiteSpeed has NO top-level `include` directive (unlike +## Apache's IncludeOptional that shared-httpd relies on). So we cannot just drop +## per-vhost files in a dir and have OLS pick them up — the listener `map` lines +## and the vhost stanzas must live IN httpd_config.conf. This script is the +## "include" OLS lacks: it concatenates the panel's per-site pieces into one +## valid httpd_config.conf, then the caller issues `lswsctrl restart`. +## (Empirically established 2026-06-10 — see the OLS-tier PoC.) +## +## Per-site contract — the panel writes, for each site, a directory: +## $SITES_ROOT//vhconf.conf (rendered from configs/shared-ols/vhconf.tpl) +## $SITES_ROOT//site.meta (shell: VHNAME, VHROOT, DOMAINS="a.com,www.a.com") +## This script turns each into a `virtualhost {configFile}` stanza + a listener +## `map` line. A site dir missing either file is skipped (logged). +## +## Idempotent: always rebuilds from the stock config, so re-runs never compound. +set -euo pipefail + +LSWS_CONF=/usr/local/lsws/conf +TPL_DIR=${TPL_DIR:-/etc/shared-ols-templates} +SITES_ROOT=${SITES_ROOT:-$LSWS_CONF/shared-sites} +LSCACHE_ROOT=${LSCACHE_ROOT:-/var/lscache} +CERT_FILE=${CERT_FILE:-$LSWS_CONF/cert/shared-ols.crt} +KEY_FILE=${KEY_FILE:-$LSWS_CONF/cert/shared-ols.key} +export LSCACHE_ROOT + +OUT="$LSWS_CONF/httpd_config.conf" +STOCK="/usr/local/lsws/.conf/httpd_config.conf" + +mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT" + +## --- 1. start from a pristine stock config (idempotent) --- +if [ ! -f "$STOCK" ]; then + ## Some image builds keep the only copy at conf/; snapshot it once so future + ## renders have a clean base to strip. + mkdir -p "$(dirname "$STOCK")" + cp "$OUT" "$STOCK" +fi + +## --- 2. strip stock blocks that conflict or would run PHP LOCALLY --- +## extProcessor lsphp (autoStart 1, uds) + the server scriptHandler are removed +## so this server NEVER executes PHP itself — all PHP goes to remote sidecars. +## listener HTTP/HTTPS + vhTemplate docker are removed (we add our own). +awk ' + /^listener HTTP \{/ { skip=1; next } + /^listener HTTPS \{/ { skip=1; next } + /^vhTemplate docker ?\{/ { skip=1; next } + /^extProcessor lsphp ?\{/{ skip=1; next } + /^scriptHandler ?\{/ { skip=1; next } + skip && /^\}/ { skip=0; next } + !skip { print } +' "$STOCK" > "$OUT" + +## --- 3. append our server-level base (real-IP, cache module, no local PHP) --- +{ + echo "" + envsubst '${LSCACHE_ROOT}' < "$TPL_DIR/httpd_config_base.tpl" +} >> "$OUT" + +## --- 4. emit per-site vhost stanzas + collect listener map lines --- +maps="" +site_count=0 +for meta in "$SITES_ROOT"/*/site.meta; do + [ -e "$meta" ] || continue + sdir=$(dirname "$meta") + VHNAME=""; VHROOT=""; DOMAINS="" + # shellcheck source=/dev/null + . "$meta" + if [ -z "$VHNAME" ] || [ -z "$VHROOT" ] || [ -z "$DOMAINS" ] || [ ! -f "$sdir/vhconf.conf" ]; then + echo "render-shared-ols: skipping $sdir (incomplete: VHNAME/VHROOT/DOMAINS/vhconf.conf)" >&2 + continue + fi + { + echo "" + echo "virtualhost ${VHNAME} {" + echo " vhRoot ${VHROOT}" + echo " configFile ${sdir}/vhconf.conf" + echo " allowSymbolLink 1" + echo " enableScript 1" + echo " restrained 1" + echo "}" + } >> "$OUT" + maps="${maps} map ${VHNAME} ${DOMAINS}"$'\n' + site_count=$((site_count + 1)) +done + +## --- 5. ALWAYS add a health vhost mapped to the catch-all so the server is +## valid with zero customer sites and HAProxy health checks (which hit by IP / +## unknown Host) get a 200. Exact-domain maps above win over this '*'. --- +{ + echo "" + echo "virtualhost _health {" + echo " vhRoot /usr/local/lsws/shared-ols-health" + echo " configFile /usr/local/lsws/shared-ols-health/vhconf.conf" + echo " allowSymbolLink 1" + echo " enableScript 0" + echo "}" +} >> "$OUT" +maps="${maps} map _health *"$'\n' + +## --- 6. listeners (HTTP :80 + HTTPS :443 self-signed) carrying ALL maps. +## HAProxy terminates real TLS and connects to this tier on :443 ssl verify +## none (same as shared-httpd), so :443 needs a cert — self-signed is fine. --- +{ + echo "" + echo "listener shared_http {" + echo " address *:80" + echo " secure 0" + printf '%s' "$maps" + echo "}" + echo "" + echo "listener shared_https {" + echo " address *:443" + echo " secure 1" + echo " keyFile ${KEY_FILE}" + echo " certFile ${CERT_FILE}" + printf '%s' "$maps" + echo "}" +} >> "$OUT" + +chown -R lsadm:nogroup "$LSWS_CONF" 2>/dev/null || true +echo "render-shared-ols: wrote $OUT ($site_count customer vhost(s) + health)"