From ec168411bfc57dd3b115a5467f85e5973c40bd91 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Fri, 10 Jul 2026 13:14:48 -0400 Subject: [PATCH 1/2] Make ops/install.sh idempotent and symlink config from the repo Rework the installer so it doubles as an updater: - Idempotent throughout: `install -d` for dirs, apt source lists rewritten in place, `gpg --dearmor --yes`, fallback cert generated only if missing, repo cloned or fast-forwarded, and `ln -sfn` symlinks. - Config is now symlinked straight from the checked-out repo instead of wget-ing raw files from GitHub. /etc/openresty/{nginx.conf,autossl.conf, sites-enabled/000-proxy}, the targetinfo.lua lualib, and the systemd unit all point at $REPO_DIR/ops, so an update is just `git pull` + reload with no re-copying. This also drops the external t42-common raw-file dependency (autossl.conf / proxy.conf now come from this repo). - Validate `openresty -t` before reloading so a bad config can't take the proxy down; reload if running else restart. - Fix prior bugs: stray `curl sudo apt-get update`, duplicate openssl cert line, and `cd ../nodejs` (now cd $REPO_DIR/nodejs). Require root; add a BRANCH override (default master). Co-Authored-By: Claude Opus 4.8 --- ops/install.sh | 126 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 37 deletions(-) mode change 100644 => 100755 ops/install.sh diff --git a/ops/install.sh b/ops/install.sh old mode 100644 new mode 100755 index d2da2aa..baa833f --- a/ops/install.sh +++ b/ops/install.sh @@ -1,52 +1,104 @@ +#!/usr/bin/env bash +# +# Install / update the Theta42 proxy on a fresh or existing host. +# +# This script is idempotent: run it to install, and re-run it to update. It +# installs system dependencies (Node, OpenResty, Redis, Lua modules), checks +# out (or fast-forwards) the repo at $REPO_DIR, and symlinks the OpenResty and +# systemd config straight from the repo. Because the config is symlinked, an +# update is just "git pull + reload" -- the files under /etc always track the +# repo, so there is nothing to re-copy. +# +# Usage: sudo ./install.sh +set -euo pipefail -apt install libpam0g-dev build-essential redis-server luarocks --no-install-recommends wget gnupg ca-certificates curl git -y +REPO_URL="https://github.com/theta42/proxy.git" +REPO_DIR="/var/www/proxy" +BRANCH="${BRANCH:-master}" +NODE_MAJOR=22 -curl sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -NODE_MAJOR=20 -echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list +if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root (try: sudo $0)" >&2 + exit 1 +fi -sudo apt-get install -y nodejs +# Symlink $1 -> $2, replacing whatever is already at $2 (idempotent). +link(){ + ln -sfn "$1" "$2" + echo "linked $2 -> $1" +} -echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list +echo "==> Base packages" +apt-get update +apt-get install -y --no-install-recommends \ + libpam0g-dev build-essential redis-server luarocks \ + wget gnupg ca-certificates curl git lsb-release -sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates #cert +echo "==> Node.js ${NODE_MAJOR}.x apt source" +install -d -m 0755 /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \ + > /etc/apt/sources.list.d/nodesource.list -wget -O - https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg #importing gpg key - -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list > /dev/null +echo "==> OpenResty apt source" +wget -qO- https://openresty.org/package/pubkey.gpg \ + | gpg --dearmor --yes -o /usr/share/keyrings/openresty.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" \ + > /etc/apt/sources.list.d/openresty.list -sudo apt-get update +echo "==> Install Node.js + OpenResty" +apt-get update +apt-get install -y nodejs openresty -sudo apt-get -y install openresty +echo "==> Lua modules" +luarocks install lua-resty-auto-ssl +luarocks install luasocket +echo "==> Fallback SSL cert" +install -d /etc/ssl +if [ ! -f /etc/ssl/resty-auto-ssl-fallback.crt ]; then + openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \ + -subj '/CN=sni-support-required-for-valid-ssl' \ + -keyout /etc/ssl/resty-auto-ssl-fallback.key \ + -out /etc/ssl/resty-auto-ssl-fallback.crt +else + echo "fallback cert already present, skipping" +fi -sudo luarocks install lua-resty-auto-ssl -sudo luarocks install luasocket +echo "==> Repo checkout at ${REPO_DIR} (branch ${BRANCH})" +install -d "$(dirname "$REPO_DIR")" +if [ -d "$REPO_DIR/.git" ]; then + git -C "$REPO_DIR" fetch --prune origin + git -C "$REPO_DIR" checkout "$BRANCH" + git -C "$REPO_DIR" pull --ff-only origin "$BRANCH" +else + git clone --branch "$BRANCH" "$REPO_URL" "$REPO_DIR" +fi -mkdir /etc/ssl/ +echo "==> Symlink config from the repo" +install -d /etc/openresty/sites-enabled /var/log/nginx +link "$REPO_DIR/ops/nginx_conf/nginx.conf" /etc/openresty/nginx.conf +link "$REPO_DIR/ops/nginx_conf/autossl.conf" /etc/openresty/autossl.conf +link "$REPO_DIR/ops/nginx_conf/proxy.conf" /etc/openresty/sites-enabled/000-proxy +link "$REPO_DIR/ops/nginx_conf/targetinfo.lua" /usr/local/openresty/lualib/targetinfo.lua +link "$REPO_DIR/ops/proxy.service" /etc/systemd/system/proxy.service -openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -subj '/CN=sni-support-required-for-valid-ssl' -keyout /etc/ssl/resty-auto-ssl-fallback.key -out /etc/ssl/resty-auto-ssl-fallback.crt +echo "==> Node dependencies" +( cd "$REPO_DIR/nodejs" && npm install ) -openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -subj '/CN=sni-support-required-for-valid-ssl' -keyout /etc/ssl/resty-auto-ssl-fallback.key -out /etc/ssl/resty-auto-ssl-fallback.crt +echo "==> Services" +systemctl daemon-reload +systemctl enable --now proxy.service +systemctl restart proxy.service -mkdir /etc/openresty/sites-enabled/ -wget -q https://raw.githubusercontent.com/theta42/proxy/refs/heads/master/ops/nginx_conf/nginx.conf -O /etc/openresty/nginx.conf -wget -q https://raw.githubusercontent.com/theta42/t42-common/master/templates/openresty/autossl.conf.erb -O /etc/openresty/autossl.conf -wget -q https://raw.githubusercontent.com/theta42/t42-common/master/templates/openresty/010-proxy.conf.erb -O /etc/openresty/sites-enabled/000-proxy -wget -q https://raw.githubusercontent.com/theta42/proxy/master/ops/proxy.service -O /etc/systemd/system/proxy.service +# Validate the OpenResty config before (re)starting so a bad edit can't take +# the proxy down; reload if already running, otherwise start it. +if openresty -t; then + systemctl reload openresty 2>/dev/null || systemctl restart openresty +else + echo "openresty config test FAILED -- not reloading" >&2 + exit 1 +fi -mkdir /var/log/nginx -mkdir /var/www - -cd /var/www - -git clone https://github.com/theta42/proxy.git - -cd proxy/nodejs -npm install - -wget -q https://raw.githubusercontent.com/theta42/proxy/refs/heads/master/ops/nginx_conf/targetinfo.lua -O /usr/local/openresty/lualib/targetinfo.lua - -systemctl start proxy.service -systemctl enable proxy.service +echo "==> Done. Update later with: sudo BRANCH=${BRANCH} $0" From 0cca3730fbe5b31022a8ad3e543a2fb9b29f04e1 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Fri, 10 Jul 2026 13:23:31 -0400 Subject: [PATCH 2/2] Make install.sh CI/CD-friendly: deterministic force-sync to remote The installer is meant to be run by CI/CD with no human writes on prod, so updates should mirror the repo exactly rather than refuse on local drift: - Replace `git pull --ff-only` with fetch + `checkout -B origin/$BRANCH` + `reset --hard` + `clean -fd` so the box always matches origin/$BRANCH. - Set GIT_TERMINAL_PROMPT=0 so a missing/expired credential fails fast in CI instead of hanging on a prompt. - npm ci --omit=dev (lockfile, production-only) with a plain-install fallback. - Allow REPO_URL / REPO_DIR / BRANCH to be overridden from the environment. Co-Authored-By: Claude Opus 4.8 --- ops/install.sh | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/ops/install.sh b/ops/install.sh index baa833f..71b1598 100755 --- a/ops/install.sh +++ b/ops/install.sh @@ -3,17 +3,23 @@ # Install / update the Theta42 proxy on a fresh or existing host. # # This script is idempotent: run it to install, and re-run it to update. It -# installs system dependencies (Node, OpenResty, Redis, Lua modules), checks -# out (or fast-forwards) the repo at $REPO_DIR, and symlinks the OpenResty and +# installs system dependencies (Node, OpenResty, Redis, Lua modules), force-syncs +# the repo at $REPO_DIR to its remote branch, and symlinks the OpenResty and # systemd config straight from the repo. Because the config is symlinked, an -# update is just "git pull + reload" -- the files under /etc always track the -# repo, so there is nothing to re-copy. +# update is just "sync the repo + reload" -- the files under /etc always track +# the repo, so there is nothing to re-copy. # -# Usage: sudo ./install.sh +# Intended to be driven by CI/CD with no human writes on prod: the checkout is +# hard-reset to origin/$BRANCH on every run, so the box deterministically mirrors +# the repo (any drift on the box is discarded). +# +# Usage: sudo ./install.sh (override with REPO_URL=, REPO_DIR=, BRANCH=) set -euo pipefail +# Never block on an interactive git credential prompt in CI. +export GIT_TERMINAL_PROMPT=0 -REPO_URL="https://github.com/theta42/proxy.git" -REPO_DIR="/var/www/proxy" +REPO_URL="${REPO_URL:-https://github.com/theta42/proxy.git}" +REPO_DIR="${REPO_DIR:-/var/www/proxy}" BRANCH="${BRANCH:-master}" NODE_MAJOR=22 @@ -69,9 +75,12 @@ fi echo "==> Repo checkout at ${REPO_DIR} (branch ${BRANCH})" install -d "$(dirname "$REPO_DIR")" if [ -d "$REPO_DIR/.git" ]; then + # Force the box to match the remote branch exactly. No human edits configs + # on prod, so discarding local drift is the desired, deterministic behavior. git -C "$REPO_DIR" fetch --prune origin - git -C "$REPO_DIR" checkout "$BRANCH" - git -C "$REPO_DIR" pull --ff-only origin "$BRANCH" + git -C "$REPO_DIR" checkout -B "$BRANCH" "origin/$BRANCH" + git -C "$REPO_DIR" reset --hard "origin/$BRANCH" + git -C "$REPO_DIR" clean -fd else git clone --branch "$BRANCH" "$REPO_URL" "$REPO_DIR" fi @@ -85,7 +94,9 @@ link "$REPO_DIR/ops/nginx_conf/targetinfo.lua" /usr/local/openresty/lualib/targe link "$REPO_DIR/ops/proxy.service" /etc/systemd/system/proxy.service echo "==> Node dependencies" -( cd "$REPO_DIR/nodejs" && npm install ) +# Deterministic, production-only install from the lockfile. Falls back to a +# plain install if the lockfile and manifest are out of step. +( cd "$REPO_DIR/nodejs" && { npm ci --omit=dev || npm install --omit=dev; } ) echo "==> Services" systemctl daemon-reload