Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .ci-dockerfiles/pg-ssl/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM postgres:14.5

LABEL org.opencontainers.image.source="https://github.com/ponylang/postgres"

RUN openssl req -new -x509 -days 3650 -nodes \
-out /var/lib/postgresql/server.crt \
-keyout /var/lib/postgresql/server.key \
-subj '/CN=localhost' && \
chmod 600 /var/lib/postgresql/server.key && \
chown postgres:postgres /var/lib/postgresql/server.crt /var/lib/postgresql/server.key

CMD ["postgres", "-c", "ssl=on", \
"-c", "ssl_cert_file=/var/lib/postgresql/server.crt", \
"-c", "ssl_key_file=/var/lib/postgresql/server.key"]
15 changes: 15 additions & 0 deletions .ci-dockerfiles/pg-ssl/build-and-push.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash

set -o errexit
set -o nounset

#
# *** You should already be logged in to GitHub Container Registry when you run
# this ***
#

DOCKERFILE_DIR="$(dirname "$0")"

docker build --pull \
-t ghcr.io/ponylang/postgres-ci-pg-ssl:latest "${DOCKERFILE_DIR}"
docker push ghcr.io/ponylang/postgres-ci-pg-ssl:latest
30 changes: 30 additions & 0 deletions .github/workflows/build-ci-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build CI Image

on:
workflow_dispatch:

permissions:
packages: write

jobs:
pg-ssl:
runs-on: ubuntu-latest

name: pg-ssl
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx
# v3.10.0
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
with:
version: v0.23.0
- name: Login to GitHub Container Registry
# v2.2.0
uses: docker/login-action@5139682d94efc37792e6b54386b5b470a68a4737
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
run: bash .ci-dockerfiles/pg-ssl/build-and-push.bash
16 changes: 15 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,19 @@ jobs:
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: md5
POSTGRES_INITDB_ARGS: "--auth-host=md5"
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
postgres-ssl:
image: ghcr.io/ponylang/postgres-ci-pg-ssl:latest
env:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: md5
POSTGRES_INITDB_ARGS: "--auth-host=md5"
options: >-
--health-cmd pg_isready
--health-interval 10s
Expand All @@ -65,6 +77,8 @@ jobs:
env:
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_SSL_HOST: postgres-ssl
POSTGRES_SSL_PORT: 5432
POSTGRES_USERNAME: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DATABASE: postgres
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ make ssl=3.0.x # build and run all tests
make unit-tests ssl=3.0.x # unit tests only (no postgres needed)
make integration-tests ssl=3.0.x # integration tests (needs postgres)
make build-examples ssl=3.0.x # compile examples
make start-pg-container # docker postgres:14.5 on port 5432
make stop-pg-container # stop docker container
make start-pg-containers # docker postgres:14.5 on ports 5432 (plain) and 5433 (SSL)
make stop-pg-containers # stop docker containers
```

SSL version is mandatory. Tests run with `--sequential`. Integration tests require a running PostgreSQL 14.5 with MD5 auth (user: postgres, password: postgres, database: postgres). Environment variables: `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_USERNAME`, `POSTGRES_PASSWORD`, `POSTGRES_DATABASE`.
SSL version is mandatory. Tests run with `--sequential`. Integration tests require running PostgreSQL 14.5 containers with MD5 auth (user: postgres, password: postgres, database: postgres) — one plain on port 5432 and one with SSL on port 5433. Environment variables: `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_SSL_HOST`, `POSTGRES_SSL_PORT`, `POSTGRES_USERNAME`, `POSTGRES_PASSWORD`, `POSTGRES_DATABASE`.

## Dependencies

Expand Down Expand Up @@ -139,6 +139,7 @@ Tests live in the main `postgres/` package (private test classes).
- PreparedStatement/PrepareAndClose, PreparedStatement/PrepareFails, PreparedStatement/PrepareAfterClose
- PreparedStatement/CloseNonexistent, PreparedStatement/PrepareDuplicateName
- PreparedStatement/MixedWithSimpleAndPrepared
- SSL/Connect, SSL/Authenticate, SSL/Query, SSL/Refused

Test helpers: `_ConnectionTestConfiguration` reads env vars with defaults. Several test message builder classes (`_Incoming*TestMessage`) construct raw protocol bytes for unit tests.

Expand All @@ -150,7 +151,7 @@ Test helpers: `_ConnectionTestConfiguration` reads env vars with defaults. Sever

## Roadmap

**SSL/TLS negotiation** is implemented. Pass `SSLRequired(sslctx)` to `Session.create()` to enable. Design: [discussion #76](https://github.com/ponylang/postgres/discussions/76). Full feature roadmap: [discussion #72](https://github.com/ponylang/postgres/discussions/72). Integration tests with a real SSL-enabled PostgreSQL are not yet in place.
**SSL/TLS negotiation** is implemented. Pass `SSLRequired(sslctx)` to `Session.create()` to enable. Design: [discussion #76](https://github.com/ponylang/postgres/discussions/76). Full feature roadmap: [discussion #72](https://github.com/ponylang/postgres/discussions/72). CI uses `ghcr.io/ponylang/postgres-ci-pg-ssl:latest` as a service container for SSL integration tests; built via `build-ci-image.yml` workflow dispatch or locally via `.ci-dockerfiles/pg-ssl/build-and-push.bash`.

## Supported PostgreSQL Features

Expand Down Expand Up @@ -331,4 +332,5 @@ examples/ssl-query/ssl-query-example.pony # SSL-encrypted query with SSLRequired
examples/prepared-query/prepared-query-example.pony # PreparedQuery with params and NULL
examples/named-prepared-query/named-prepared-query-example.pony # Named prepared statements with reuse
examples/crud/crud-example.pony # Multi-query CRUD workflow
.ci-dockerfiles/pg-ssl/ # Dockerfile for SSL-enabled PostgreSQL CI container
```
13 changes: 7 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ else
PONYC = $(COMPILE_WITH) --debug
endif

ifeq (,$(filter $(MAKECMDGOALS),clean docs realclean start-pg-container stop-pg-container TAGS))
ifeq (,$(filter $(MAKECMDGOALS),clean docs realclean start-pg-containers stop-pg-containers TAGS))
ifeq ($(ssl), 3.0.x)
SSL = -Dopenssl_3.0.x
else ifeq ($(ssl), 1.1.x)
Expand Down Expand Up @@ -84,12 +84,13 @@ $(coverage_binary): $(SOURCE_FILES) | $(COVERAGE_DIR)
$(GET_DEPENDENCIES_WITH)
$(PONYC) --debug -o $(COVERAGE_DIR) $(SRC_DIR)

start-pg-container:
start-pg-containers:
@docker run --name pg -e POSTGRES_DB=postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_HOST_AUTH_METHOD=md5 -e POSTGRES_INITDB_ARGS="--auth-host=md5" -p 5432:5432 -d postgres:14.5
@docker run --name pg-ssl -e POSTGRES_DB=postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_HOST_AUTH_METHOD=md5 -e POSTGRES_INITDB_ARGS="--auth-host=md5" -v $(CURDIR)/assets/test-cert.pem:/var/lib/postgresql/server.crt:ro -v $(CURDIR)/assets/test-key.pem:/var/lib/postgresql/server.key.orig:ro -p 5433:5432 -d --entrypoint sh postgres:14.5 -c "cp /var/lib/postgresql/server.key.orig /var/lib/postgresql/server.key && chmod 600 /var/lib/postgresql/server.key && chown postgres:postgres /var/lib/postgresql/server.key && exec docker-entrypoint.sh postgres -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key"

stop-pg-container:
@docker stop pg
@docker rm pg
stop-pg-containers:
@docker stop pg pg-ssl
@docker rm pg pg-ssl

all: test

Expand All @@ -99,4 +100,4 @@ $(BUILD_DIR):
$(COVERAGE_DIR):
mkdir -p $(COVERAGE_DIR)

.PHONY: all build-examples clean docs TAGS test coverage start-pg-container stop-pg-container
.PHONY: all build-examples clean docs TAGS test coverage start-pg-containers stop-pg-containers
132 changes: 132 additions & 0 deletions postgres/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ actor \nodoc\ Main is TestList
test(_TestCloseNonexistent)
test(_TestPrepareDuplicateName)
test(_TestPreparedStatementMixedWithSimpleAndPrepared)
test(_TestSSLConnect)
test(_TestSSLAuthenticate)
test(_TestSSLQueryResults)
test(_TestSSLRefused)

class \nodoc\ iso _TestAuthenticate is UnitTest
"""
Expand Down Expand Up @@ -215,6 +219,8 @@ actor \nodoc\ _ConnectTestNotify is SessionStatusNotify
class \nodoc\ val _ConnectionTestConfiguration
let host: String
let port: String
let ssl_host: String
let ssl_port: String
let username: String
let password: String
let database: String
Expand All @@ -223,6 +229,8 @@ class \nodoc\ val _ConnectionTestConfiguration
let e = EnvVars(vars)
host = try e("POSTGRES_HOST")? else "127.0.0.1" end
port = try e("POSTGRES_PORT")? else "5432" end
ssl_host = try e("POSTGRES_SSL_HOST")? else host end
ssl_port = try e("POSTGRES_SSL_PORT")? else "5433" end
username = try e("POSTGRES_USERNAME")? else "postgres" end
password = try e("POSTGRES_PASSWORD")? else "postgres" end
database = try e("POSTGRES_DATABASE")? else "postgres" end
Expand Down Expand Up @@ -1098,3 +1106,127 @@ actor \nodoc\ _SSLSuccessTestServer
_tcp_connection.send(auth_ok)
_tcp_connection.send(ready)
end

class \nodoc\ iso _TestSSLConnect is UnitTest
"""
Verifies that connecting with SSLRequired to a PostgreSQL server with SSL
enabled results in a successful connection.
"""
fun name(): String =>
"integration/SSL/Connect"

fun apply(h: TestHelper) =>
let info = _ConnectionTestConfiguration(h.env.vars)

let sslctx = recover val
SSLContext
.> set_client_verify(false)
.> set_server_verify(false)
end

let session = Session(
lori.TCPConnectAuth(h.env.root),
_ConnectTestNotify(h, true),
info.ssl_host,
info.ssl_port,
info.username,
info.password,
info.database,
SSLRequired(sslctx))

h.dispose_when_done(session)
h.long_test(5_000_000_000)

class \nodoc\ iso _TestSSLAuthenticate is UnitTest
"""
Verifies that connecting with SSLRequired to a PostgreSQL server with SSL
enabled allows successful MD5 authentication over the encrypted connection.
"""
fun name(): String =>
"integration/SSL/Authenticate"

fun apply(h: TestHelper) =>
let info = _ConnectionTestConfiguration(h.env.vars)

let sslctx = recover val
SSLContext
.> set_client_verify(false)
.> set_server_verify(false)
end

let session = Session(
lori.TCPConnectAuth(h.env.root),
_AuthenticateTestNotify(h, true),
info.ssl_host,
info.ssl_port,
info.username,
info.password,
info.database,
SSLRequired(sslctx))

h.dispose_when_done(session)
h.long_test(5_000_000_000)

class \nodoc\ iso _TestSSLQueryResults is UnitTest
"""
Verifies that queries can be executed and results received over an
SSL-encrypted connection.
"""
fun name(): String =>
"integration/SSL/Query"

fun apply(h: TestHelper) =>
let info = _ConnectionTestConfiguration(h.env.vars)

let sslctx = recover val
SSLContext
.> set_client_verify(false)
.> set_server_verify(false)
end

let client = _ResultsIncludeOriginatingQueryReceiver(h)

let session = Session(
lori.TCPConnectAuth(h.env.root),
client,
info.ssl_host,
info.ssl_port,
info.username,
info.password,
info.database,
SSLRequired(sslctx))

h.dispose_when_done(session)
h.long_test(5_000_000_000)

class \nodoc\ iso _TestSSLRefused is UnitTest
"""
Verifies that connecting with SSLRequired to a PostgreSQL server that does
not support SSL results in pg_session_connection_failed. Unlike the
SSLNegotiation/Refused unit test which uses a mock server, this tests
against a real PostgreSQL instance.
"""
fun name(): String =>
"integration/SSL/Refused"

fun apply(h: TestHelper) =>
let info = _ConnectionTestConfiguration(h.env.vars)

let sslctx = recover val
SSLContext
.> set_client_verify(false)
.> set_server_verify(false)
end

let session = Session(
lori.TCPConnectAuth(h.env.root),
_ConnectTestNotify(h, false),
info.host,
info.port,
info.username,
info.password,
info.database,
SSLRequired(sslctx))

h.dispose_when_done(session)
h.long_test(5_000_000_000)