Skip to content
73 changes: 7 additions & 66 deletions ansible/files/postgres_prestart.sh.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,85 +6,26 @@ log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

check_extensions_file() {
handle_extension_versions() {
local extensions_file="/etc/adminapi/pg-extensions.json"
if [ ! -f "$extensions_file" ]; then
log "extensions: No extensions file found, skipping extensions versions check"
return 0 #if file not found, skip
fi
if [ ! -r "$extensions_file" ]; then
log "extensions: Cannot read extensions file"
return 1 #a true error, we should be able to read file
fi
return 0
}

switch_extension_version() {
local extension_name="$1"
local version="$2"

# Use BIN_PATH environment variable or default to /var/lib/postgresql/.nix-profile
: ${BIN_PATH:="/var/lib/postgresql/.nix-profile"}

local switch_script="$BIN_PATH/bin/switch_${extension_name}_version"

if [ ! -x "$switch_script" ]; then
log "$extension_name: No version switch script available at $switch_script, skipping"
log "extensions: no manifest at $extensions_file, skipping"
return 0
fi

log "$extension_name: Switching to version $version"
# Run directly as root since we're already running as root
"$switch_script" "$version"
local exit_code=$?
if [ $exit_code -eq 0 ]; then
log "$extension_name: Version switch completed successfully"
if update-site-extensions "$extensions_file"; then
log "extensions: profile updated"
else
log "$extension_name: Version switch failed with exit code $exit_code"
fi
return $exit_code
}

handle_extension_versions() {
if ! check_extensions_file; then
return
fi

local extensions_file="/etc/adminapi/pg-extensions.json"

# Get all extension names from the JSON file
local extensions
extensions=$(jq -r 'keys[]' "$extensions_file" 2>/dev/null)

if [ -z "$extensions" ]; then
log "extensions: No extensions found in configuration"
return
log "extensions: update failed"
return 1
fi

# Iterate through each extension
while IFS= read -r extension_name; do
# Get the version for this extension
local version
version=$(jq -r --arg ext "$extension_name" '.[$ext] // empty' "$extensions_file")

if [ -z "$version" ]; then
log "$extension_name: No version specified, skipping"
continue
fi

log "$extension_name: Found version $version in extensions file"

# Don't fail if version switch fails - just log and continue
switch_extension_version "$extension_name" "$version" || log "$extension_name: Version switch failed but continuing"

done <<< "$extensions"
}

main() {
log "Starting prestart script"

# 1. Handle all extension versions from config file
handle_extension_versions
handle_extension_versions || return 1

log "Prestart script completed"
}
Expand Down
2 changes: 1 addition & 1 deletion ansible/files/postgresql_config/postgresql.service
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ RestartSec=5
OOMScoreAdjust=-1000
EnvironmentFile=-/etc/environment.d/postgresql.env
LimitNOFILE=16384
ReadOnlyPaths=/etc
ReadOnlyPaths=/etc /usr/local
InaccessiblePaths=/root -/var/lib/supabase -/var/lib/supabase-admin-agent -/var/cache/supabase-admin-agent -/opt/saltstack -/etc/salt
AppArmorProfile=-sbpostgres
[Install]
Expand Down
85 changes: 85 additions & 0 deletions nix/ext/tests/site-extensions-upgrade.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{ self, pkgs }:
let
testLib = import ./lib.nix { inherit self pkgs; };
inherit (pkgs) lib;
system = pkgs.pkgsLinux.stdenv.hostPlatform.system;

pinnedExtension = "pgaudit";
pinnedVersion = "99.0";

placeholderPackage = pkgs.pkgsLinux.runCommand "pgaudit-${pinnedVersion}-generic" { } ''
mkdir -p "$out/lib" "$out/share/postgresql/extension"
echo "default_version = '${pinnedVersion}'" > "$out/share/postgresql/extension/pgaudit.control"
'';
fakeCatalog = pkgs.pkgsLinux.writeText "pg-extensions-catalog.json" (
builtins.toJSON {
${pinnedExtension} = {
${pinnedVersion} = "${placeholderPackage}";
};
}
);

prestartScript = pkgs.pkgsLinux.writeShellScript "postgres-prestart-site-extensions" (
builtins.readFile ../../../ansible/files/postgres_prestart.sh.j2
);
in
pkgs.testers.runNixOSTest {
name = "site-extensions-upgrade";
nodes.server = {
imports = [ (testLib.makeSupabaseTestConfig { majorVersion = "17"; }) ];

environment.systemPackages = [ self.packages.${system}.update-site-extensions ];
systemd.services.postgresql.path = [
self.packages.${system}.update-site-extensions
pkgs.pkgsLinux.nix
];
systemd.services.postgresql.environment.PG_EXTENSIONS_CATALOG = "${fakeCatalog}";

# No network in the VM, so pre-realize it.
virtualisation.additionalPaths = [ placeholderPackage ];

environment.etc."adminapi/pg-extensions.json".text = builtins.toJSON {
${pinnedExtension} = pinnedVersion;
};

systemd.services.postgresql.serviceConfig.ExecStartPre = lib.mkAfter [
("+" + prestartScript)
];
};

testScript = ''
pinned_extension = "${pinnedExtension}"
pinned_version = "${pinnedVersion}"

start_all()

server.wait_for_unit("multi-user.target")
server.wait_for_unit("postgresql.service")
server.wait_for_unit("supabase-db-init.service")

with subtest("extensions manifest present"):
server.succeed("test -r /etc/adminapi/pg-extensions.json")

with subtest("restart postgresql to trigger the real prestart path"):
server.execute("systemctl restart postgresql.service")
server.wait_for_unit("postgresql.service")

with subtest("prestart ran update-site-extensions"):
unit_log = server.succeed("journalctl -u postgresql.service -o cat --no-pager")
assert "Starting prestart script" in unit_log, unit_log
assert "extensions: profile updated" in unit_log, unit_log

with subtest("site-extensions profile has the pinned version"):
profile_contents = server.succeed(
"nix-env --profile /nix/var/nix/profiles/site-extensions -q"
).strip()
assert pinned_extension in profile_contents, profile_contents

with subtest("EXPECTED TO FAIL: profile isn't on dynamic_library_path/extension_control_path"):
default_version = server.succeed(
"psql -U supabase_admin -d postgres -tAc "
f"\"select default_version from pg_available_extensions where name = '{pinned_extension}'\""
).strip()
assert default_version == pinned_version, f"expected {pinned_version}, got {default_version}"
'';
}
21 changes: 9 additions & 12 deletions nix/packages/extension-catalog.nix
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@
makeWrapper ${self'.packages.site-extensions-resolve}/bin/site-extensions-resolve \
"$out/bin/site-extensions-resolve" \
--set PG_EXTENSIONS_CATALOG "$out/share/pg-extensions-catalog.json"
makeWrapper ${self'.packages.site-extensions-update}/bin/site-extensions-update \
"$out/bin/site-extensions-update" \
makeWrapper ${self'.packages.update-site-extensions}/bin/update-site-extensions \
"$out/bin/update-site-extensions" \
--set PG_EXTENSIONS_CATALOG "$out/share/pg-extensions-catalog.json"
''
)
Expand Down Expand Up @@ -151,19 +151,16 @@
'';
};

# Takes manifest json as argument.
# Downloads paths and installs them as an env into the profile, replacing all existing ones.
site-extensions-update = pkgs.writeShellApplication {
name = "site-extensions-update";
runtimeInputs = [
self'.packages.site-extensions-resolve
pkgs.nix
];
# Downloads the manifest's paths and installs them as the profile's env, replacing all existing ones.
# nix comes from the environment.
update-site-extensions = pkgs.writeShellApplication {
name = "update-site-extensions";
runtimeInputs = [ self'.packages.site-extensions-resolve ];
text = ''
manifest="''${1:?Usage: $0 path-to/pg-extensions.json}"
profile="''${NIX_PROFILE:-/nix/var/nix/profiles/site-extensions}"
profile="/nix/var/nix/profiles/site-extensions"
readarray -t paths < <(site-extensions-resolve "$manifest")
nix-store -r --option stalled-download-timeout 120 "''${paths[@]}" >/dev/null
nix-store --realise --option stalled-download-timeout 120 "''${paths[@]}" >/dev/null
nix-env --profile "$profile" --install "''${paths[@]}" --remove-all
'';
};
Expand Down
Loading