diff --git a/default.nix b/default.nix index e9c3353779..09af0f1d01 100644 --- a/default.nix +++ b/default.nix @@ -44,7 +44,6 @@ let allOverlays.checked-shell-script allOverlays.gitignore (allOverlays.haskell-packages { inherit compiler; }) - allOverlays.slocat ]; # Evaluated expression of the Nixpkgs repository. diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 4a5f88643d..f8d00c1ae9 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -3,5 +3,4 @@ checked-shell-script = import ./checked-shell-script; gitignore = import ./gitignore.nix; haskell-packages = import ./haskell-packages.nix; - slocat = import ./slocat.nix; } diff --git a/nix/overlays/slocat.nix b/nix/overlays/slocat.nix deleted file mode 100644 index 42f89fc345..0000000000 --- a/nix/overlays/slocat.nix +++ /dev/null @@ -1,13 +0,0 @@ -_: prev: -{ - slocat = prev.buildGoModule { - name = "slocat"; - src = prev.fetchFromGitHub { - owner = "robx"; - repo = "slocat"; - rev = "52e7512c6029fd00483e41ccce260a3b4b9b3b64"; - sha256 = "sha256-qn6luuh5wqREu3s8RfuMCP5PKdS2WdwPrujRYTpfzQ8="; - }; - vendorHash = null; - }; -} diff --git a/nix/tools/withTools.nix b/nix/tools/withTools.nix index a63e8f1fb6..9dde744d31 100644 --- a/nix/tools/withTools.nix +++ b/nix/tools/withTools.nix @@ -6,9 +6,10 @@ , postgresqlVersions , postgrest , python3Packages -, slocat +, socat , writeText , writers +, toxiproxy }: let withTmpDb = @@ -55,6 +56,8 @@ let export PGDATA="$tmpdir/db" export PGHOST="$tmpdir/socket" + PGPORT=$(${randomPort}) + export PGPORT export PGUSER export PGDATABASE export PGRST_DB_SCHEMAS @@ -63,8 +66,15 @@ let HBA_FILE="$tmpdir/pg_hba.conf" echo "local $PGDATABASE some_protected_user password" > "$HBA_FILE" - echo "local $PGDATABASE all trust" >> "$HBA_FILE" - echo "local replication all trust" >> "$HBA_FILE" + { + echo "local $PGDATABASE all trust" + echo "local replication all trust" + echo "host $PGDATABASE some_protected_user localhost scram-sha-256" + echo "host $PGDATABASE all localhost trust" + } >> "$HBA_FILE" + + UNIX_PGHOST="$PGHOST" + export TCP_PGHOST="localhost" log "Initializing database cluster..." # We try to make the database cluster as independent as possible from the host @@ -81,7 +91,7 @@ let # On MacOS, it's 104 chars # See: https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket - pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \ + pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"$TCP_PGHOST\" -c hba_file=$HBA_FILE -k $UNIX_PGHOST -c log_statement=\"all\" " \ >> "$setuplog" log "Creating a minimally privileged $PGUSER connection role..." @@ -94,6 +104,7 @@ let replica_slot="replica_$RANDOM" replica_dir="$tmpdir/$replica_slot" replica_host="$tmpdir/socket_$replica_slot" + replica_port=$(${randomPort}) mkdir -p "$replica_host" @@ -106,15 +117,16 @@ let log "Starting replica on $replica_host" - pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \ + pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"$TCP_PGHOST\" -c port=$replica_port -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \ >> "$setuplog" >&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres" >&2 echo "${commandName}: You can tail the replica logs with: tail -f $replica_dblog" export PGREPLICAHOST="$replica_host" + export PGREPLICAPORT="$replica_port" export PGREPLICASLOT="$replica_slot" - export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST" + export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST&port=$replica_port,$PGPORT" fi # shellcheck disable=SC2317 @@ -203,24 +215,8 @@ let withTmpDir = true; } '' - delay="''${PGDELAY:-0ms}" - echo "delaying data to/from postgres by $delay" - - REALPGHOST="$PGHOST" - export PGHOST="$tmpdir/socket" - mkdir -p "$PGHOST" - - ${slocat}/bin/slocat -delay "$delay" -src "$PGHOST/.s.PGSQL.5432" -dst "$REALPGHOST/.s.PGSQL.5432" & - SLOCAT_PID=$! - # shellcheck disable=SC2317 - stop_slocat() { - kill "$SLOCAT_PID" || true - wait "$SLOCAT_PID" || true - } - trap stop_slocat EXIT - sleep 1 # should wait for socket file to appear instead - - ("$_arg_command" "''${_arg_leftovers[@]}") + proxyPort=''$(${randomPort}) + (${withToxiproxyServer} ${withToxiproxyProxy} -l "$TCP_PGHOST:$proxyPort" -u "$TCP_PGHOST:$PGPORT" ${withToxiproxyLatency} "$PGDELAY" env "PGHOST=$TCP_PGHOST" "PGPORT=$proxyPort" "$_arg_command" "''${_arg_leftovers[@]}") ''; withSlowPgrst = @@ -239,25 +235,40 @@ let workingDir = "/"; redirectTixFiles = false; withTmpDir = true; + withPath = [ socat ]; } '' - delay="''${PGRST_DELAY:-0ms}" - echo "delaying data to/from PostgREST by $delay" + # Toxiproxy cannot connect to unix sockets + # so we start socat to make Pgrst available on TCP + # and another socat to make Toxiproxy available on Unix socket + # TODO maybe simply change withPgrst to start PostgREST on TCP socket + socatPort=''$(${randomPort}) + proxyPort=''$(${randomPort}) + slowPgrstSocket="$tmpdir/postgrest.socket" + + socat TCP-LISTEN:"$socatPort",reuseaddr,fork UNIX-CONNECT:"$PGRST_SERVER_UNIX_SOCKET" & + UPSTREAM_SOCAT_PID=$! + echo "Started upstream socat" + + socat UNIX-LISTEN:"$slowPgrstSocket",fork,unlink-early TCP:localhost:"$proxyPort" & + DOWNSTREAM_SOCAT_PID=$! + echo "Started downstream socat" - REAL_PGRST_SERVER_UNIX_SOCKET="$PGRST_SERVER_UNIX_SOCKET" - export PGRST_SERVER_UNIX_SOCKET="$tmpdir/postgrest.socket" - - ${slocat}/bin/slocat -delay "$delay" -src "$PGRST_SERVER_UNIX_SOCKET" -dst "$REAL_PGRST_SERVER_UNIX_SOCKET" & - SLOCAT_PID=$! # shellcheck disable=SC2317 - stop_slocat() { - kill "$SLOCAT_PID" || true - wait "$SLOCAT_PID" || true + stop() { + kill "$UPSTREAM_SOCAT_PID" || true + kill "$DOWNSTREAM_SOCAT_PID" || true + wait "$UPSTREAM_SOCAT_PID" || true + wait "$DOWNSTREAM_SOCAT_PID" || true } - trap stop_slocat EXIT - sleep 1 # should wait for socket file to appear instead + trap stop EXIT - ("$_arg_command" "''${_arg_leftovers[@]}") + sleep 1 + + # Execute command with Toxiproxy and environment having + # PGRST_SERVER_UNIX_SOCKET variable unset + # and PGRST_SERVER_HOST/PGRST_SERVER_PORT set to localhost/$proxyPort + (${withToxiproxyServer} ${withToxiproxyProxy} -l "localhost:$proxyPort" -u "localhost:$socatPort" env "PGRST_SERVER_UNIX_SOCKET=$slowPgrstSocket" "PGRST_SERVER_HOST=localhost" "PGRST_SERVER_PORT=$proxyPort" ${withToxiproxyLatency} "$PGRST_DELAY" "$_arg_command" "''${_arg_leftovers[@]}") ''; withGit = @@ -447,6 +458,117 @@ let libraries = [ python3Packages.pandas python3Packages.tabulate python3Packages.psutil ]; } (builtins.readFile ./monitor_pid.py); + + randomPort = + writers.writePython3 "postgrest-random-port" + { + # Quick one-liner: ignore linting errors + flakeIgnore = [ "E702" "W292" "E501" ]; + } + ''import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()''; + + withToxiproxyProxy = + checkedShellScript + { + name = "postgrest-with-toxiproxy-proxy"; + docs = "Run with Toxiproxy proxy created. Proxy name passed as TOXI_PROXY_NAME env variable."; + args = + [ + "ARG_POSITIONAL_SINGLE([command], [Command to run])" + "ARG_LEFTOVERS([command arguments])" + "ARG_OPTIONAL_SINGLE([listen], [l], [Proxy will listen on this address])" + "ARG_OPTIONAL_SINGLE([upstream], [u], [Proxy will forward to this address])" + ]; + positionalCompletion = "_command"; + workingDir = "/"; + withPath = [ toxiproxy ]; + } + '' + proxyname="tp$RANDOM" + toxiproxy-cli create -l "$_arg_listen" -u "$_arg_upstream" "$proxyname" + + # shellcheck disable=SC2317 + stop () { + toxiproxy-cli delete "$proxyname" || true + } + trap stop EXIT + + (TOXI_PROXY_NAME="$proxyname" "$_arg_command" "''${_arg_leftovers[@]}") + ''; + + withToxiproxyLatency = + checkedShellScript + { + name = "postgrest-with-toxiproxy-latency"; + docs = "Run with Toxiproxy latency toxic for both upstream and downstream"; + args = + [ + "ARG_POSITIONAL_SINGLE([latency], [Latency])" + "ARG_POSITIONAL_SINGLE([command], [Command to run])" + "ARG_LEFTOVERS([command arguments])" + "ARG_USE_ENV([TOXI_PROXY_NAME], [], [Toxiproxy proxy name to create toxic in])" + ]; + positionalCompletion = "_command"; + workingDir = "/"; + withPath = [ toxiproxy ]; + } + '' + proxyname="$TOXI_PROXY_NAME" + upstream_toxicname="toxic$RANDOM" + downstream_toxicname="toxic$RANDOM" + # calculate delay in milliseconds + # version accepting only milliseconds + # TODO implement delay in seconds/minutes/hours + #read -r delay unit <<<''$(echo "$_arg_latency" | sed -r 's/([0-9]+)(.*)/\1 \2/g') + delay=''$(echo "$_arg_latency" | sed -r 's/([0-9]+)(.*)/\1/g') + + toxiproxy-cli toxic add -t latency --upstream -n "$upstream_toxicname" -a latency="$delay" "$proxyname" + toxiproxy-cli toxic add -t latency --downstream -n "$downstream_toxicname" -a latency="$delay" "$proxyname" + + # shellcheck disable=SC2317 + stop () { + toxiproxy-cli toxic delete -n "$downstream_toxicname" "$proxyname" || true + toxiproxy-cli toxic delete -n "$upstream_toxicname" "$proxyname" || true + } + trap stop EXIT + + ("$_arg_command" "''${_arg_leftovers[@]}") + ''; + + withToxiproxyServer = + let + commandName = "postgrest-with-toxiproxy-server"; + in + checkedShellScript + { + name = commandName; + docs = "Run with toxiproxy-server"; + args = + [ + "ARG_POSITIONAL_SINGLE([command], [Command to run])" + "ARG_LEFTOVERS([command arguments])" + ]; + positionalCompletion = "_command"; + workingDir = "/"; + withPath = [ toxiproxy ]; + } + '' + if ! test -v TOXI_PROXY; then + export TOXI_PROXY="" + LOG_LEVEL=error toxiproxy-server& + TOXIPROXY_PID=$! + sleep 1 # give the server a moment to start + + # shellcheck disable=SC2317 + stop () { + kill "$TOXIPROXY_PID" || true + wait "$TOXIPROXY_PID" || true + } + trap stop EXIT + fi + ("$_arg_command" "''${_arg_leftovers[@]}") + ''; + in buildToolbox { diff --git a/test/io/conftest.py b/test/io/conftest.py index 87880724f1..78dd9a0d31 100644 --- a/test/io/conftest.py +++ b/test/io/conftest.py @@ -9,8 +9,9 @@ def dburi(): "Postgres database connection URI." dbname = os.environ["PGDATABASE"] host = os.environ["PGHOST"] + port = os.environ["PGPORT"] user = os.environ["PGUSER"] - return f"postgresql://?dbname={dbname}&host={host}&user={user}".encode() + return f"postgresql://?dbname={dbname}&host={host}&port={port}&user={user}".encode() @pytest.fixture @@ -19,6 +20,7 @@ def baseenv(): return { "PGDATABASE": os.environ["PGDATABASE"], "PGHOST": os.environ["PGHOST"], + "PGPORT": os.environ["PGPORT"], "PGUSER": os.environ["PGUSER"], } @@ -51,6 +53,7 @@ def replicaenv(defaultenv): **defaultenv, **conf, "PGHOST": os.environ["PGREPLICAHOST"] + "," + os.environ["PGHOST"], + "PGPORT": os.environ["PGREPLICAPORT"] + "," + os.environ["PGPORT"], "PGREPLICASLOT": os.environ["PGREPLICASLOT"], }, } @@ -76,6 +79,7 @@ def metapostgrest(): env = { "PGDATABASE": os.environ["PGDATABASE"], "PGHOST": os.environ["PGHOST"], + "PGPORT": os.environ["PGPORT"], "PGUSER": role, "PGRST_DB_ANON_ROLE": role, "PGRST_DB_CONFIG": "true", diff --git a/test/io/test_auth.py b/test/io/test_auth.py index 82ed07706e..0b4ac7d006 100644 --- a/test/io/test_auth.py +++ b/test/io/test_auth.py @@ -165,7 +165,7 @@ def relativeSeconds(sec): def test_fail_with_invalid_password(defaultenv): "Connecting with an invalid password should fail without retries." - uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass' + uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&port={defaultenv["PGPORT"]}&user=some_protected_user&password=invalid_pass' env = {**defaultenv, "PGRST_DB_URI": uri} with run(env=env, wait_for_readiness=False) as postgrest: exitCode = wait_until_exit(postgrest) diff --git a/test/io/test_io.py b/test/io/test_io.py index 79b386062e..a5dab00312 100644 --- a/test/io/test_io.py +++ b/test/io/test_io.py @@ -1653,7 +1653,7 @@ def test_log_listener_connection_start(defaultenv): # Check for the listener start message containing host and port # Do not check if pg version is displayed properly as it is tricky to test it assert any( - f'"{defaultenv["PGHOST"]}:5432" and listening for database notifications on the "pgrst" channel' + f'"{defaultenv["PGHOST"]}:{defaultenv["PGPORT"]}" and listening for database notifications on the "pgrst" channel' in line for line in output )