diff --git a/nix/checks.nix b/nix/checks.nix index cc879b0a3e..ace0e51e63 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -933,6 +933,7 @@ ) // pkgs.lib.optionalAttrs (pkgs.stdenv.isLinux) { site = import ./tests/site.nix { inherit self pkgs; }; + site-roundtrip = import ./tests/site-roundtrip.nix { inherit self pkgs; }; inherit (self'.packages) postgresql_15_debug postgresql_15_src diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 6fa7d97e06..d0968e0982 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -69,6 +69,12 @@ http-mock-server = pkgs.callPackage ./http-mock-server.nix { }; image-size-analyzer = pkgs.callPackage ./image-size-analyzer.nix { }; local-infra-bootstrap = pkgs.callPackage ./local-infra-bootstrap.nix { }; + manifest-diff = pkgs.callPackage ./manifest-diff.nix { }; + manifest-snapshot = pkgs.callPackage ./manifest-snapshot.nix { }; + site-profile-sanity = pkgs.callPackage ./site-profile-sanity.nix { }; + site-profile-roundtrip = pkgs.callPackage ./site-profile-test.nix { + inherit (self'.packages) manifest-snapshot site-profile-sanity; + }; mecab-naist-jdic = pkgs.callPackage ./mecab-naist-jdic.nix { }; migrate-tool = pkgs.callPackage ./migrate-tool.nix { psql_15 = self'.packages."psql_15/bin"; }; overlayfs-on-package = pkgs.callPackage ./overlayfs-on-package.nix { }; diff --git a/nix/packages/manifest-diff.nix b/nix/packages/manifest-diff.nix new file mode 100644 index 0000000000..1892837669 --- /dev/null +++ b/nix/packages/manifest-diff.nix @@ -0,0 +1,48 @@ +{ + writeShellApplication, + diffutils, + gnused, + coreutils, +}: +writeShellApplication { + name = "manifest-diff"; + runtimeInputs = [ + diffutils + gnused + coreutils + ]; + text = '' + if [ "$#" -lt 4 ]; then + echo "Usage: manifest-diff " >&2 + exit 1 + fi + MANIFEST_PATH="$1" + CURRENT_MANIFEST_PATH="$2" + PUBLISH_HINT="$3" + DIFF_PATH="$4" + + if [ -s "$MANIFEST_PATH" ]; then + mv "$MANIFEST_PATH" /tmp/baseline-raw.txt + BASELINE_VERSION=$(sed -n 's/^# version: //p' /tmp/baseline-raw.txt | head -1) + tail -n +2 /tmp/baseline-raw.txt > /tmp/baseline.txt + else + touch /tmp/baseline.txt + BASELINE_VERSION="" + fi + mv "$CURRENT_MANIFEST_PATH" "$MANIFEST_PATH" + { + if [ -z "$BASELINE_VERSION" ]; then + echo "No baseline cached yet. $PUBLISH_HINT" + elif diff -q /tmp/baseline.txt "$MANIFEST_PATH" > /dev/null; then + echo "No changes vs baseline $BASELINE_VERSION." + else + echo "baseline: $BASELINE_VERSION" + diff \ + --old-line-format='-%L' \ + --new-line-format='+%L' \ + --unchanged-group-format=$'... %dn unchanged\n' \ + /tmp/baseline.txt "$MANIFEST_PATH" || true + fi + } | tee "$DIFF_PATH" + ''; +} diff --git a/nix/packages/manifest-snapshot.nix b/nix/packages/manifest-snapshot.nix new file mode 100644 index 0000000000..715e9bdacb --- /dev/null +++ b/nix/packages/manifest-snapshot.nix @@ -0,0 +1,13 @@ +{ + writeShellApplication, + libarchive, + coreutils, +}: +writeShellApplication { + name = "manifest-snapshot"; + runtimeInputs = [ + libarchive + coreutils + ]; + text = builtins.readFile ../../testinfra/manifest-snapshot.sh; +} diff --git a/nix/packages/site-profile-sanity.nix b/nix/packages/site-profile-sanity.nix new file mode 100644 index 0000000000..6c4bace9ce --- /dev/null +++ b/nix/packages/site-profile-sanity.nix @@ -0,0 +1,9 @@ +{ + writeShellApplication, + coreutils, +}: +writeShellApplication { + name = "site-profile-sanity"; + runtimeInputs = [ coreutils ]; + text = builtins.readFile ../../testinfra/site-profile-sanity.sh; +} diff --git a/nix/packages/site-profile-test.nix b/nix/packages/site-profile-test.nix new file mode 100644 index 0000000000..3e27291132 --- /dev/null +++ b/nix/packages/site-profile-test.nix @@ -0,0 +1,19 @@ +{ + writeShellApplication, + coreutils, + diffutils, + gnugrep, + manifest-snapshot, + site-profile-sanity, +}: +writeShellApplication { + name = "site-profile-roundtrip"; + runtimeInputs = [ + coreutils + diffutils + gnugrep + manifest-snapshot + site-profile-sanity + ]; + text = builtins.readFile ../../testinfra/site-profile-roundtrip.sh; +} diff --git a/nix/tests/site-roundtrip.nix b/nix/tests/site-roundtrip.nix new file mode 100644 index 0000000000..d4d3a9481f --- /dev/null +++ b/nix/tests/site-roundtrip.nix @@ -0,0 +1,58 @@ +{ self, pkgs }: +let + system = pkgs.pkgsLinux.stdenv.hostPlatform.system; + site-env-17 = self.packages.${system}."site-env-17"; + roundtrip = self.packages.${system}.site-profile-roundtrip; + psql_17 = self.legacyPackages.${system}."psql_17".bin; + previousSiteEnv = pkgs.pkgsLinux.buildEnv { + name = "site-env-17-previous"; + paths = [ + (pkgs.pkgsLinux.writeTextDir "share/site-env-previous" "previous") + ]; + }; + pgRestart = pkgs.pkgsLinux.writeShellScript "pg-restart" '' + exec su postgres -c '${psql_17}/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log restart' + ''; + psql = pkgs.pkgsLinux.writeShellScript "psql" '' + exec ${psql_17}/bin/psql -h /tmp -U postgres -X -At "$@" + ''; + pgConf = pkgs.writeText "postgresql-test.conf" '' + dynamic_library_path = '/nix/var/nix/profiles/site/lib/postgresql:$libdir' + session_preload_libraries = 'supautils' + unix_socket_directories = '/tmp' + ''; +in +pkgs.testers.runNixOSTest { + name = "site-roundtrip"; + nodes.machine = + { ... }: + { + environment.systemPackages = [ + site-env-17 + roundtrip + ]; + virtualisation.additionalPaths = [ + psql_17 + previousSiteEnv + ]; + users.users.postgres = { + isSystemUser = true; + group = "postgres"; + shell = pkgs.bash; + }; + users.groups.postgres = { }; + }; + testScript = '' + machine.succeed("install -d -o postgres -g postgres /tmp/pgdata") + machine.succeed("su postgres -c '${psql_17}/bin/initdb -D /tmp/pgdata'") + machine.succeed("install -o postgres -g postgres ${pgConf} /tmp/pgdata/postgresql.conf") + machine.succeed("su postgres -c '${psql_17}/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log start'") + + machine.succeed("nix-env --profile /nix/var/nix/profiles/site --set ${previousSiteEnv}") + + machine.succeed( + "PG_RESTART=${pgRestart} PSQL='${psql} -c' " + "site-profile-roundtrip ${site-env-17} /tmp/manifests" + ) + ''; +} diff --git a/testinfra/manifest-snapshot.sh b/testinfra/manifest-snapshot.sh new file mode 100755 index 0000000000..3c800a8957 --- /dev/null +++ b/testinfra/manifest-snapshot.sh @@ -0,0 +1,40 @@ +#!/bin/sh +set -eu + +INSTALLED=0 +if ! command -v bsdtar >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then + DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libarchive-tools >/dev/null + elif command -v apk >/dev/null 2>&1; then + apk add --no-cache libarchive-tools >/dev/null + fi + INSTALLED=1 +fi + +cd / +# shellcheck disable=SC2086 +bsdtar --format=mtree \ + --options='!all,type,mode,uid,gid,sha256digest,link' \ + --exclude=./dev --exclude=./proc --exclude=./run --exclude=./sys --exclude=./tmp --exclude=./var/log --exclude=./data \ + ${MANIFEST_EXCLUDES:-} \ + -cf - . 2>/dev/null | sort + +if [ "$INSTALLED" = 1 ]; then + if command -v apt-get >/dev/null 2>&1; then + DEBIAN_FRONTEND=noninteractive apt-get remove -y --purge libarchive-tools >/dev/null + elif command -v apk >/dev/null 2>&1; then + apk del libarchive-tools >/dev/null + fi +fi + +echo '--- units ---' +systemctl list-unit-files --no-pager 2>/dev/null | sort +echo '--- users ---' +getent passwd | sort +echo '--- groups ---' +getent group | sort +echo '--- nft ---' +nft list ruleset 2>/dev/null || true +echo '--- sysctl ---' +sysctl -a 2>/dev/null | sort diff --git a/testinfra/site-profile-roundtrip.sh b/testinfra/site-profile-roundtrip.sh new file mode 100755 index 0000000000..20079a5fa3 --- /dev/null +++ b/testinfra/site-profile-roundtrip.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Usage: site-profile-roundtrip +# Run as root on a host booted from the latest AMI. +set -euo pipefail + +SITE_ENV="${1:?Usage: $0 }" +OUT="${2:?Usage: $0 }" +PROFILE=/nix/var/nix/profiles/site +SANITY="${SITE_PROFILE_SANITY:-site-profile-sanity}" + +mkdir -p "$OUT" +export MANIFEST_EXCLUDES="--exclude=./nix/store --exclude=./nix/.ro-store --exclude=./nix/.rw-store --exclude=./nix/var/nix/db" + +# Store paths, the nix db, and generation links change on every update by design. +# Sysctl is dropped as it holds live counters. The store is skipped at capture, as hashing it dominates the runtime. +snapshot() { + manifest-snapshot | sed '/^--- sysctl ---$/,$d' | grep -v -e '^\./nix/store' -e '^\./nix/var/nix/db' -e '^\./nix/var/nix/profiles/.*-[0-9]*-link' -e '^\./nix/var/nix/gcroots/auto/' -e '^\./nix/var/nix/temproots' >"$OUT/$1.txt" +} + +update() { + nix-env --profile "$PROFILE" --set "$SITE_ENV" + "$PROFILE/bin/activate" +} + +expect_same() { + if ! diff -u "$OUT/$1.txt" "$OUT/$2.txt" >"$OUT/$1-vs-$2.diff"; then + echo "FAIL: $1 and $2 differ" + cat "$OUT/$1-vs-$2.diff" + exit 1 + fi + echo "ok: $1 == $2" +} + +snapshot m0 + +update +snapshot m1 +diff -u "$OUT/m0.txt" "$OUT/m1.txt" >"$OUT/update.diff" || true +"$SANITY" updated + +update +snapshot m2 +expect_same m1 m2 + +if [[ -x "$PROFILE/bin/deactivate" ]]; then + "$PROFILE/bin/deactivate" +fi +nix-env --profile "$PROFILE" --rollback +snapshot m3 +expect_same m0 m3 +"$SANITY" rolled-back diff --git a/testinfra/site-profile-sanity.sh b/testinfra/site-profile-sanity.sh new file mode 100755 index 0000000000..447cc50d3f --- /dev/null +++ b/testinfra/site-profile-sanity.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Usage: site-profile-sanity +# PG_RESTART and PSQL override the AMI defaults. +set -euo pipefail + +MODE="${1:?Usage: $0 }" +read -ra PG_RESTART <<<"${PG_RESTART:-systemctl restart postgresql}" +read -ra PSQL <<<"${PSQL:-sudo -u postgres psql -X -At -c}" + +"${PG_RESTART[@]}" + +[[ "$("${PSQL[@]}" 'select 1')" == 1 ]] + +if [[ $MODE == updated ]]; then + "${PSQL[@]}" "load 'supautils'" + "${PSQL[@]}" 'show dynamic_library_path' | grep -q /nix/var/nix/profiles/site/lib/postgresql +fi + +echo "ok: sanity $MODE"