Skip to content

Commit 902c605

Browse files
whummerclaudepurcell
authored
extract common extension utils into a separate package (#123)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Steve Purcell <steve@sanityinc.com>
1 parent 75c4d82 commit 902c605

34 files changed

+1536
-475
lines changed

.github/workflows/miniflare.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ jobs:
3333
docker pull localstack/localstack-pro &
3434
pip install localstack localstack-ext
3535
36-
# TODO remove
37-
mkdir ~/.localstack; echo '{"token":"test"}' > ~/.localstack/auth.json
38-
3936
branchName=${GITHUB_HEAD_REF##*/}
4037
if [ "$branchName" = "" ]; then branchName=main; fi
4138
echo "Installing from branch name $branchName"

.github/workflows/utils.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: LocalStack Extensions Utils Tests
2+
3+
on:
4+
push:
5+
paths:
6+
- utils/**
7+
branches:
8+
- main
9+
pull_request:
10+
paths:
11+
- .github/workflows/utils.yml
12+
- utils/**
13+
workflow_dispatch:
14+
15+
jobs:
16+
unit-tests:
17+
name: Run Unit Tests
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 5
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: "3.11"
28+
29+
- name: Install dependencies
30+
run: |
31+
cd utils
32+
pip install -e .[dev,test]
33+
34+
- name: Lint
35+
run: |
36+
cd utils
37+
make lint
38+
39+
- name: Run unit tests
40+
run: |
41+
cd utils
42+
make test-unit
43+
44+
integration-tests:
45+
name: Run Integration Tests
46+
runs-on: ubuntu-latest
47+
timeout-minutes: 10
48+
steps:
49+
- name: Checkout
50+
uses: actions/checkout@v4
51+
52+
- name: Set up Python
53+
uses: actions/setup-python@v5
54+
with:
55+
python-version: "3.11"
56+
57+
- name: Install dependencies
58+
run: |
59+
cd utils
60+
pip install -e .[dev,test]
61+
62+
- name: Run integration tests
63+
run: |
64+
docker pull moul/grpcbin &
65+
cd utils
66+
make test-integration

.github/workflows/wiremock.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
pip install localstack terraform-local awscli-local[ver1]
4040
4141
make install
42+
make lint
4243
make dist
4344
localstack extensions -v install file://$(ls ./dist/localstack_wiremock-*.tar.gz)
4445

flake.nix

Lines changed: 0 additions & 22 deletions
This file was deleted.

paradedb/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ entrypoints: venv ## Generate plugin entrypoints for Python package
3434
$(VENV_RUN); python -m plux entrypoints
3535

3636
format: ## Run ruff to format the codebase
37-
$(VENV_RUN); python -m ruff format .; make lint
37+
$(VENV_RUN); python -m ruff format .; python -m ruff check --fix .
3838

3939
lint: ## Run ruff to lint the codebase
4040
$(VENV_RUN); python -m ruff check --output-format=full .
Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import os
2-
import logging
2+
import socket
33

4-
from localstack_paradedb.utils.docker import DatabaseDockerContainerExtension
5-
6-
LOG = logging.getLogger(__name__)
4+
from localstack_extensions.utils.docker import ProxiedDockerContainerExtension
5+
from localstack import config
76

87
# Environment variables for configuration
98
ENV_POSTGRES_USER = "PARADEDB_POSTGRES_USER"
109
ENV_POSTGRES_PASSWORD = "PARADEDB_POSTGRES_PASSWORD"
1110
ENV_POSTGRES_DB = "PARADEDB_POSTGRES_DB"
12-
ENV_POSTGRES_PORT = "PARADEDB_POSTGRES_PORT"
1311

1412
# Default values
1513
DEFAULT_POSTGRES_USER = "myuser"
@@ -18,7 +16,7 @@
1816
DEFAULT_POSTGRES_PORT = 5432
1917

2018

21-
class ParadeDbExtension(DatabaseDockerContainerExtension):
19+
class ParadeDbExtension(ProxiedDockerContainerExtension):
2220
name = "paradedb"
2321

2422
# Name of the Docker image to spin up
@@ -31,7 +29,13 @@ def __init__(self):
3129
ENV_POSTGRES_PASSWORD, DEFAULT_POSTGRES_PASSWORD
3230
)
3331
postgres_db = os.environ.get(ENV_POSTGRES_DB, DEFAULT_POSTGRES_DB)
34-
postgres_port = int(os.environ.get(ENV_POSTGRES_PORT, DEFAULT_POSTGRES_PORT))
32+
postgres_port = DEFAULT_POSTGRES_PORT
33+
34+
# Store configuration for connection info
35+
self.postgres_user = postgres_user
36+
self.postgres_password = postgres_password
37+
self.postgres_db = postgres_db
38+
self.postgres_port = postgres_port
3539

3640
# Environment variables to pass to the container
3741
env_vars = {
@@ -40,31 +44,70 @@ def __init__(self):
4044
"POSTGRES_DB": postgres_db,
4145
}
4246

47+
def _tcp_health_check():
48+
"""Check if ParadeDB port is accepting connections."""
49+
self._check_tcp_port(self.container_host, self.postgres_port)
50+
4351
super().__init__(
4452
image_name=self.DOCKER_IMAGE,
4553
container_ports=[postgres_port],
4654
env_vars=env_vars,
55+
health_check_fn=_tcp_health_check,
56+
tcp_ports=[postgres_port], # Enable TCP proxying through gateway
4757
)
4858

49-
# Store configuration for connection info
50-
self.postgres_user = postgres_user
51-
self.postgres_password = postgres_password
52-
self.postgres_db = postgres_db
53-
self.postgres_port = postgres_port
59+
def tcp_connection_matcher(self, data: bytes) -> bool:
60+
"""
61+
Identify PostgreSQL/ParadeDB connections by protocol handshake.
62+
63+
PostgreSQL can start with either:
64+
1. SSL request: protocol code 80877103 (0x04D2162F)
65+
2. Startup message: protocol version 3.0 (0x00030000)
66+
67+
Both use the same format:
68+
- 4 bytes: message length
69+
- 4 bytes: protocol version/code
70+
"""
71+
if len(data) < 8:
72+
return False
73+
74+
# Check for SSL request (80877103 = 0x04D2162F)
75+
if data[4:8] == b"\x04\xd2\x16\x2f":
76+
return True
77+
78+
# Check for protocol version 3.0 (0x00030000)
79+
if data[4:8] == b"\x00\x03\x00\x00":
80+
return True
81+
82+
return False
83+
84+
def _check_tcp_port(self, host: str, port: int, timeout: float = 2.0) -> None:
85+
"""Check if a TCP port is accepting connections."""
86+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
87+
sock.settimeout(timeout)
88+
try:
89+
sock.connect((host, port))
90+
sock.close()
91+
except (socket.timeout, socket.error) as e:
92+
raise AssertionError(f"Port {port} not ready: {e}")
5493

5594
def get_connection_info(self) -> dict:
5695
"""Return connection information for ParadeDB."""
57-
info = super().get_connection_info()
58-
info.update(
59-
{
60-
"database": self.postgres_db,
61-
"user": self.postgres_user,
62-
"password": self.postgres_password,
63-
"port": self.postgres_port,
64-
"connection_string": (
65-
f"postgresql://{self.postgres_user}:{self.postgres_password}"
66-
f"@{self.container_host}:{self.postgres_port}/{self.postgres_db}"
67-
),
68-
}
69-
)
70-
return info
96+
# Clients should connect through the LocalStack gateway
97+
gateway_host = "paradedb.localhost.localstack.cloud"
98+
gateway_port = config.LOCALSTACK_HOST.port
99+
100+
return {
101+
"host": gateway_host,
102+
"database": self.postgres_db,
103+
"user": self.postgres_user,
104+
"password": self.postgres_password,
105+
"port": gateway_port,
106+
"connection_string": (
107+
f"postgresql://{self.postgres_user}:{self.postgres_password}"
108+
f"@{gateway_host}:{gateway_port}/{self.postgres_db}"
109+
),
110+
# Also include container connection details for debugging
111+
"container_host": self.container_host,
112+
"container_port": self.postgres_port,
113+
}

paradedb/localstack_paradedb/utils/docker.py

Lines changed: 0 additions & 144 deletions
This file was deleted.

0 commit comments

Comments
 (0)