Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
SHELL := /bin/bash
VENV_DIR ?= .venv
VENV_RUN = . $(VENV_DIR)/bin/activate
PIP_CMD ?= pip
PYTHON_CMD ?= python
TEST_REQS ?= requirements-test.txt
LINT_REQS ?= requirements-lint.txt

PG_TEST_CONTAINER ?= pg-proxy-local-tests
PG_TEST_IMAGE ?= postgres:16
PG_TEST_PORT ?= 55432
PG_TEST_USER ?= postgres
PG_TEST_PASSWORD ?= postgres
PG_TEST_DB ?= postgres

usage: ## Show this help
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
Expand All @@ -14,4 +25,42 @@ install: ## Install dependencies in local virtualenv folder
publish: ## Publish the library to the central PyPi repository
($(VENV_RUN); pip install twine; python ./setup.py sdist && twine upload dist/*)

.PHONY: usage install clean publish test lint
install-test: install ## Install test dependencies in local virtualenv
($(VENV_RUN); $(PIP_CMD) install -r $(TEST_REQS))

install-lint: install ## Install lint dependencies in local virtualenv
($(VENV_RUN); $(PIP_CMD) install -r $(LINT_REQS))

lint: install-lint ## Format code with ruff
$(VENV_DIR)/bin/ruff format postgresql_proxy tests plugins

test: ## Start local PostgreSQL container and run all tests
@set -euo pipefail; \
cleanup() { docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true; }; \
trap cleanup EXIT INT TERM; \
docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true; \
docker run --name $(PG_TEST_CONTAINER) \
-e POSTGRES_USER=$(PG_TEST_USER) \
-e POSTGRES_PASSWORD=$(PG_TEST_PASSWORD) \
-e POSTGRES_DB=$(PG_TEST_DB) \
-p $(PG_TEST_PORT):5432 \
-d $(PG_TEST_IMAGE) >/dev/null; \
for i in $$(seq 1 45); do \
if docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
echo "PostgreSQL ready on 127.0.0.1:$(PG_TEST_PORT)"; \
break; \
fi; \
sleep 1; \
done; \
if ! docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
echo "PostgreSQL did not become ready in time"; \
exit 1; \
fi; \
E2E_PG_HOST=127.0.0.1 \
E2E_PG_PORT=$(PG_TEST_PORT) \
E2E_PG_USER=$(PG_TEST_USER) \
E2E_PG_PASSWORD=$(PG_TEST_PASSWORD) \
E2E_PG_DB=$(PG_TEST_DB) \
$(VENV_DIR)/bin/$(PYTHON_CMD) -m pytest -vv

.PHONY: usage install install-test install-lint clean publish test lint
119 changes: 93 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,30 @@ Serves as a proper server that Postgresql clients can connect to. Can modify pac
Currently used for rewriting queries to force proper use of postgres-hll module by external proprietary software that doesn't know about that functionality

## Installing
### Linux
1. Make sure you have [python3 and pip3 installed on your system](https://stackoverflow.com/questions/6587507/how-to-install-pip-with-python-3#6587528). It has been tested with Python3.6 but should also run on Python3.5.
2. Clone it locally and cd to that directory
```
git clone git@github.com:kfzteile24/postgresql-proxy.git
cd postgresql-proxy
```
3. Run [setup.sh](setup.sh)
```
./setup.sh
```
4. Make a copy of [config.yml.example](config.yml.example) called `config.yml` and configure your proxy instances. Create the log directories if they're not there.

Requires Python `3.13+`.

1. Clone the repository:
```bash
git clone git@github.com:localstack/postgresql-proxy.git
cd postgresql-proxy
```
2. Install dependencies into a local virtualenv:
```bash
make install
```
3. Copy the example config and edit it for your environment:
```bash
cp config.yml.example config.yml
```

## Configuring
In the `config.yml` file you can define the following things
### Plugins
A list of dynamically loaded modules that reside in the [plugins](plugins) directory. These plugins can be used in later configuration, to intercept queries, commands, or responses. View plugin documentation for example plugins for more details on how to do that.
### Settings
General application settings. Currently the following settings are used
* `log-level` - the log level for the general log. See [python logging](https://docs.python.org/3.6/library/logging.html) for more details about the logging functionality
* `log-level` - the log level for the general log. See [python logging](https://docs.python.org/3/library/logging.html) for more details about the logging functionality
* `general-log` - the location for the general log. All general messages go in there.
* `intercept-log` - the location for the intercept log. Intercepted messages and return values from various enabled plugins will be written there. This log can be quite verbose as it contains the full binary messages being circulated.

Expand All @@ -42,19 +46,22 @@ Make sure to manage the logs yourself, as they accumulate and take up disk space

Each interceptor definition must have a `plugin`, which should also be present in the [plugins](#Plugins) configuration, and a `function`, that is found directly in that module, that will be called each time with the intercepted message as a byte string, and a context variable that is an instance of the `Proxy` class, that contains connection information and other useful stuff.

## Running in testing mode
If you want to test it, do this. Otherwise scroll down for instructions on how to install it as a service
### Linux
1. Activate the virtual environment
```
source .venv/bin/activate
```
2. Run it
```
python proxy.py
```

### Changelog
## Running

Activate the virtualenv and run the proxy directly:

```bash
source .venv/bin/activate
python -m postgresql_proxy
```

Or run it without activating the venv:

```bash
.venv/bin/python -m postgresql_proxy
```

## Changelog
- v0.3.1
- Fix SSL COPY stalls by draining pending SSL buffer after recv [#11](https://github.com/localstack/postgresql-proxy/pull/11)
- Fix intermittent `BlockingIOError` on macOS during SSL negotiation
Expand All @@ -81,3 +88,63 @@ If you want to test it, do this. Otherwise scroll down for instructions on how t
- add stop() method to proxy; refactor logging
- v0.0.2
- fix socket file descriptors under Linux

## Testing

All tests require a real PostgreSQL server and are organized at the top level:

- `test_proxy.py`: proxy behavior tests (connection, SSL, hang regressions)
- `test_plugins.py`: plugin integration tests (HLL rewrite behavior)

### Prerequisites

- Python `3.13` (same version as CI)
- Docker (for local disposable PostgreSQL)
- `psql` (`postgresql-client`)
- `openssl` (SSL tests generate a temporary self-signed cert/key at runtime)

Install Python deps in the project virtualenv:

```bash
make install-test
```

### Which command should I use?

- Fastest full local run with disposable Postgres: `make test`
- Run only proxy tests (using your own Postgres): `python -m pytest tests/test_proxy.py -vv`
- Run only plugin tests: `python -m pytest tests/test_plugins.py -vv`

#### 1) Full local suite (recommended)

`make test` starts a temporary PostgreSQL container, waits for readiness, sets DB env vars, then runs:

```bash
python -m pytest -vv
```

Use it when you want one command that matches normal contributor workflow.

```bash
make test
```

#### 2) Run against an existing PostgreSQL

If you already have PostgreSQL running, set connection env vars and run the tests you need:

```bash
export E2E_PG_HOST=127.0.0.1
export E2E_PG_PORT=5432
export E2E_PG_USER=postgres
export E2E_PG_PASSWORD=postgres
export E2E_PG_DB=postgres

# Proxy tests only
python -m pytest tests/test_proxy.py -vv

# Plugin tests only
python -m pytest tests/test_plugins.py -vv
```

If PostgreSQL is not reachable, tests fail fast at startup.
34 changes: 0 additions & 34 deletions plugins/tableau_hll/test.py

This file was deleted.

3 changes: 3 additions & 0 deletions postgresql_proxy/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from postgresql_proxy.proxy import main

main()
6 changes: 5 additions & 1 deletion postgresql_proxy/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ def stop(self):
self.running = False


if __name__ == '__main__':
def main():
import importlib
import yaml
import os
Expand Down Expand Up @@ -370,3 +370,7 @@ def stop(self):
logging.info("Starting proxy instance")
proxy = Proxy(instance, plugins)
proxy.listen()


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions requirements-lint.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruff==0.15.12
2 changes: 2 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest==9.0.3
pytest-timeout==2.4.0
11 changes: 1 addition & 10 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import os
import re
import sys
from setuptools import find_packages, setup

install_requires = []
Expand All @@ -16,14 +13,8 @@
install_requires=install_requires,
zip_safe=False,
classifiers=[
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.13',
'License :: OSI Approved :: Apache Software License',
'Topic :: Software Development :: Testing',
]
Expand Down
32 changes: 32 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os

import psycopg2
import pytest


@pytest.fixture(scope="session")
def postgres_settings():
"""PostgreSQL connection settings from environment or defaults."""
return {
"host": os.environ.get("E2E_PG_HOST", "127.0.0.1"),
"port": int(os.environ.get("E2E_PG_PORT", "5432")),
"user": os.environ.get("E2E_PG_USER", "postgres"),
"password": os.environ.get("E2E_PG_PASSWORD", "postgres"),
"dbname": os.environ.get("E2E_PG_DB", "postgres"),
}


@pytest.fixture(scope="session", autouse=True)
def ensure_postgres_available(postgres_settings):
"""Ensure PostgreSQL backend is available before running any tests."""
try:
with psycopg2.connect(
connect_timeout=3, sslmode="disable", **postgres_settings
) as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1")
assert cur.fetchone() == (1,)
except Exception as err: # pragma: no cover - environment dependent
pytest.fail(
f"PostgreSQL backend is required for tests but is not reachable: {err}"
)
12 changes: 0 additions & 12 deletions tests/test_plugin.py

This file was deleted.

Loading