diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aad0d23..6058c99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,167 @@ jobs: - name: Test on PostgreSQL ${{ matrix.pg }} run: pg-build-test + # Proves extension_drop can be deployed with NO filesystem footprint at + # all, via pg_tle (AWS's Trusted Language Extensions -- a database-backed + # catalog for installing an extension with no .control file on disk; + # relevant for RDS/Aurora-style managed deployments). This is a distinct + # dimension from the `test` job above: not fresh vs. updated vs. upgraded, + # but filesystem-installed vs. registered purely through pg_tle's catalog. + # + # extension_drop REQUIRES cat_tools (see the Makefile's own `cat_tools` + # target), so this job has a complication a leaf extension's pg_tle test + # doesn't: cat_tools must ALSO be registered as a pg_tle extension, before + # extension_drop, so `CREATE EXTENSION extension_drop CASCADE` resolves + # its dependency through pg_tle too instead of failing (or, worse, silently + # resolving from a stray filesystem install of cat_tools). Every step that + # could plausibly write an extension file to disk is bracketed by + # bin/assert_fs_clean checks -- see its header for why that has to be an + # active, repeated assertion, not a one-time formality. + # + # A dedicated cluster, never shared with the `test` job above: pg_tle + # requires shared_preload_libraries, and mixing pg_tle/non-pg_tle + # extension installs on one cluster can misbehave. Also deliberately its + # own job rather than folded into any existing matrix/loop -- the entire + # point of this job is proving isolation from the filesystem, and any + # sibling loop iteration doing a real `make install` would contaminate + # that proof. + # + # Explicitly OUT OF SCOPE for this job (left as follow-up work): + # - The update path via pg_tle (ALTER EXTENSION UPDATE against a pg_tle- + # registered install). extension_drop has no prior released version to + # update FROM yet (see TEST_UPDATE_FROM's comment in the Makefile), so + # there's nothing real to exercise. + # - Binary pg_upgrade of a pg_tle-deployed extension. Heavier, separate + # concern from a fresh-install smoke test. + pg-tle-test: + # Gated behind the cheap `test` job: this job compiles pg_tle from + # source and does a chained pg_tle registration, expensive enough not to + # run against a baseline that's already broken by a failing fresh-install + # test. success() is required explicitly once a job's `if:` references + # anything -- GitHub only assumes success() as a default when no `if:` is + # written at all. + needs: [test] + if: success() + strategy: + matrix: + # Intersection of two independently-moving ranges, checked directly + # rather than assumed: extension_drop's own tested range (9.3-17, + # see the `test` job's matrix above) and pg_tle 1.5.2's supported + # PostgreSQL range (12-18, dropped PG11 -- see + # pgxntool/pgtle_versions.md). 18 isn't in the `test` job's own + # matrix yet, so it's left out here too rather than testing a PG + # major extension_drop's own baseline job doesn't cover. + pg: [17, 16, 15, 14, 13, 12] + name: 🧩 pg_tle ${{ matrix.pg }} + runs-on: ubuntu-latest + container: pgxn/pgxn-tools + env: + PG_TLE_RELEASE: "1.5.2" + steps: + - name: Start PostgreSQL ${{ matrix.pg }} + run: pg-start ${{ matrix.pg }} + - name: Check out the repo + uses: actions/checkout@v5 + - name: Install pgtap (test harness dependency) + # pgTAP is a filesystem-installed dependency of the TEST HARNESS + # itself, not part of what this job proves is pg_tle-only -- it's + # never deployed via pg_tle. Installed explicitly here, before the + # baseline snapshot below, so it's part of the accepted starting + # state (like contrib) instead of tripping the contamination check + # if something installed it lazily later. + run: make pgtap + - name: Snapshot filesystem extension control files (pre-pg_tle baseline) + run: bin/assert_fs_clean snapshot ${{ matrix.pg }} /tmp/control_baseline.txt + - name: Build and install pg_tle ${{ env.PG_TLE_RELEASE }} + # flex/bison/libkrb5-dev aren't in the pgxn-tools image; pg_tle's + # build needs them (guc-file.l, and clientauth.c includes gssapi.h). + run: | + apt-get install -y flex bison libkrb5-dev + git clone --branch v${{ env.PG_TLE_RELEASE }} --depth 1 https://github.com/aws/pg_tle.git /tmp/pg_tle + make -C /tmp/pg_tle install + - name: Enable pg_tle and restart PostgreSQL ${{ matrix.pg }} + run: | + echo "shared_preload_libraries = 'pg_tle'" >> /etc/postgresql/${{ matrix.pg }}/test/postgresql.conf + pg_ctlcluster ${{ matrix.pg }} test restart + pg_isready -t 30 + - name: Register pg_tle, then cat_tools, then extension_drop -- all against template1 + # template1, not the ambient default db: pg_tle's registration + # catalog is per-database, and createdb only inherits it because it + # copies template1 by default. Every database used below is created + # AFTER this step so it inherits all three registrations. + # + # cat_tools must be registered BEFORE extension_drop: extension_drop + # requires cat_tools, so the CASCADE install below needs cat_tools + # already resolvable through pg_tle's own catalog by the time it + # runs. cat_tools is cloned fresh at the SAME git ref the Makefile's + # own filesystem `cat_tools` target pins to (CAT_TOOLS_GIT_REF), + # read via `make print-CAT_TOOLS_GIT_REF` so the two never drift + # apart. cat_tools vendors its own pgxntool copy directly (confirmed: + # its only git submodule is an unrelated linter, so a plain `git + # clone` alone gives a working `make run-pgtle`), so registering it + # needs nothing beyond the exact same target this repo uses on + # itself right after. + run: | + psql -d template1 -c "CREATE EXTENSION pg_tle" + CAT_TOOLS_GIT_REF=$(make -s print-CAT_TOOLS_GIT_REF 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p') + git clone https://github.com/Postgres-Extensions/cat_tools.git /tmp/cat_tools_tle + # -C on both git and make, rather than `cd`: this is one continuous + # shell script (a multi-line `run:` block), so a bare `cd` here + # would still be in effect for the `make run-pgtle` below that's + # meant to run against OUR OWN checkout, not the cat_tools clone. + git -C /tmp/cat_tools_tle checkout "$CAT_TOOLS_GIT_REF" + PGDATABASE=template1 make -C /tmp/cat_tools_tle run-pgtle + PGDATABASE=template1 make run-pgtle + - name: Verify no stray extension control files landed on the filesystem + # CRITICAL: a filesystem control file silently wins over a pg_tle- + # registered extension of the same name, which would make this whole + # job a false pass without ever raising an error. Run again after + # every step below that could plausibly write extension files to + # disk -- never trust a single check to catch everything. + run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt + - name: Install extension_drop purely via pg_tle (fresh install, no filesystem trace) + # Neither cat_tools nor extension_drop is ever `make install`ed in + # this job, so a successful CASCADE install here can only be + # resolving both through pg_tle's registration, not a control file + # on disk. Checked explicitly here too (not just via the + # comprehensive check above) as a guard specifically for the two + # extensions under test, in case that check's exclude logic has a + # bug. + run: | + test ! -e /usr/share/postgresql/${{ matrix.pg }}/extension/cat_tools.control + test ! -e /usr/share/postgresql/${{ matrix.pg }}/extension/extension_drop.control + createdb extension_drop_smoke + psql -d extension_drop_smoke -c "CREATE EXTENSION extension_drop CASCADE" + - name: Verify extension_drop works when deployed via pg_tle + run: | + INSTALLED=$(psql -d extension_drop_smoke -tAc "SELECT extversion FROM pg_extension WHERE extname = 'extension_drop'") + EXPECTED=$(make -s print-PGXNVERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p') + echo "installed=$INSTALLED expected=$EXPECTED" + if [ -z "$INSTALLED" ] || [ -z "$EXPECTED" ] || [ "$INSTALLED" != "$EXPECTED" ]; then + echo "FAIL: installed='$INSTALLED' expected='$EXPECTED'"; exit 1 + fi + # A real function call, not just a successful install: exercises + # add/get/remove against an extension (pg_tle itself) that's + # genuinely present in this database, proving the pg_tle-deployed + # functions actually execute correctly, not merely that CREATE + # EXTENSION didn't error. + psql -d extension_drop_smoke -v ON_ERROR_STOP=1 -c "SELECT extension_drop__add('pg_tle', 'SELECT 1')" + psql -d extension_drop_smoke -v ON_ERROR_STOP=1 -c "SELECT * FROM extension_drop__get('pg_tle')" > /dev/null + psql -d extension_drop_smoke -v ON_ERROR_STOP=1 -c "SELECT extension_drop__remove('pg_tle')" + - name: Verify no stray extension control files after the fresh-install smoke test + # Runs AFTER the complete flow, not before: the whole point of + # pg_tle-mode testing is proving NOTHING touched the filesystem + # THROUGHOUT the flow, not merely that the environment started + # clean. + run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt + # A single stable check name for use as a required status check in branch # protection. Matrix jobs produce names like "🐘 PostgreSQL 14" that change # with the matrix; this aggregates them into one. It passes if every needed # job succeeded or was skipped (e.g. a docs-only push with paths-ignore) and # fails if any failed or were cancelled. all-checks-passed: - needs: [test] + needs: [test, pg-tle-test] if: always() runs-on: ubuntu-latest steps: diff --git a/bin/assert_fs_clean b/bin/assert_fs_clean new file mode 100755 index 0000000..93a2657 --- /dev/null +++ b/bin/assert_fs_clean @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# assert_fs_clean - Verify no stray PostgreSQL extension control files exist +# on disk, to prove an extension (and its dependencies) were deployed purely +# via pg_tle (AWS's Trusted Language Extensions), not a filesystem install. +# +# A pre-existing filesystem *.control file silently wins over a pg_tle- +# registered extension of the same name -- PostgreSQL never reports an +# error, it just quietly resolves CREATE EXTENSION from disk instead of +# pg_tle's catalog. That makes "prove pg_tle-only" a real, load-bearing +# assertion, not a formality: it must run AFTER whatever step it's guarding, +# not just before, since the whole point is confirming nothing wrote to disk +# THROUGHOUT the guarded flow, not merely that the environment started +# clean. +# +# Deliberately generic over extension name: it diffs the FULL set of +# *.control files against a baseline, so it catches a stray install of ANY +# extension (extension_drop, cat_tools, or something unrelated), not just a +# hardcoded name list -- a chained dependency doesn't need its own entry +# here. +# +# Modeled on cat_tools's bin/assert_fs_clean. +# +# USAGE: bin/assert_fs_clean [args] +# +# snapshot PG_MAJOR BASELINE_FILE +# Record the current *.control files in PG_MAJOR's extension directory +# to BASELINE_FILE. Run this BEFORE installing pg_tle (or anything +# else that could write to disk), so whatever ships by default (e.g. +# contrib, pgTAP installed as the test harness's own dependency) is +# excluded automatically -- no hardcoded exclude list to keep in sync. +# +# verify PG_MAJOR BASELINE_FILE +# Fail if any *.control file exists now that wasn't in BASELINE_FILE, +# other than pg_tle.control itself (the one legitimate filesystem +# install in this flow). Run this after EVERY step that could +# plausibly have written extension files to disk. +set -euo pipefail + +extdir_of() { echo "/usr/share/postgresql/$1/extension"; } + +snapshot() { + local pg_major=$1 baseline=$2 + find "$(extdir_of "$pg_major")" -maxdepth 1 -name '*.control' | sort > "$baseline" +} + +verify() { + local pg_major=$1 baseline=$2 + local after + after=$(mktemp) + # Bake the actual path into the trap string now (double-quoted expansion), + # rather than deferring expansion to whenever the trap fires -- a RETURN + # trap wouldn't fire at all under `set -e` (this script's own errexit), and + # a single-quoted EXIT trap referencing $after by name would break once + # this function returns and $after (declared local) goes out of scope. + trap "rm -f '$after'" EXIT + local new + find "$(extdir_of "$pg_major")" -maxdepth 1 -name '*.control' | sort > "$after" + new=$(comm -13 "$baseline" "$after" | grep -vx '.*/pg_tle\.control' || true) + if [ -n "$new" ]; then + echo "FAIL: unexpected extension control file(s) on disk (everything but pg_tle must be registered via pg_tle, not filesystem-installed):" >&2 + echo "$new" >&2 + exit 1 + fi + echo "OK: no stray extension control files on disk (PG $pg_major)" +} + +usage() { + echo "usage: bin/assert_fs_clean [args]" >&2 + echo " snapshot PG_MAJOR BASELINE_FILE" >&2 + echo " verify PG_MAJOR BASELINE_FILE" >&2 + exit 2 +} + +main() { + local cmd=${1:-} + shift || true + case "$cmd" in + snapshot) snapshot "$@" ;; + verify) verify "$@" ;; + *) usage ;; + esac +} + +main "$@"