Skip to content

Commit 75b3f77

Browse files
committed
BREAKING CHANGE: don't set connection env vars
The 'setup-postgres' action used to set libpq environment variables [1] with connection parameters so the PostgreSQL client applications [2], such as 'psql' or 'createuser', won't require any configuration before using. Unfortunately these libpq environment variables are also used by all libpq clients including database drivers such as 'psycopg'. While this may sound good, it may as well lead to undesired behaviour and unobvious issues when connection parameters are automatically pulled from environment but most not. Nevertheless, the need to easy usage of the client applications [2] is indisputable because providing a bunch of connection parameters all the time is tedious. Therefore this patch pushes requires connection parameters to the connection service file [3], so the client applications can pull the data on-demand. E.g: $ psql service=superuser -c "SELECT 1;" $ PGSERVICE=superuser createuser myuser [1] https://www.postgresql.org/docs/15/libpq-envars.html [2] https://www.postgresql.org/docs/15/reference-client.html [3] https://www.postgresql.org/docs/15/libpq-pgservice.html
1 parent 3574bd5 commit 75b3f77

File tree

4 files changed

+126
-26
lines changed

4 files changed

+126
-26
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ jobs:
3030
python3 -m pytest -vv test_action.py
3131
env:
3232
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
33+
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
3334
EXPECTED_CONNECTION_URI: postgresql://postgres:postgres@localhost:5432/postgres
35+
EXPECTED_SERVICE_NAME: postgres
3436
shell: bash
3537

3638
parametrized:
@@ -59,5 +61,7 @@ jobs:
5961
python3 -m pytest -vv test_action.py
6062
env:
6163
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
64+
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
6265
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order
66+
EXPECTED_SERVICE_NAME: yoda
6367
shell: bash

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ key features:
2424
| Username | `postgres` |
2525
| Password | `postgres` |
2626
| Database | `postgres` |
27+
| Service | `postgres` |
2728

2829
#### User permissions
2930

@@ -53,7 +54,57 @@ steps:
5354

5455
- run: pytest -vv tests/
5556
env:
56-
DATABASE_URI: ${{ steps.postgres.outputs.connection-uri }}
57+
CONNECTION_STR: ${{ steps.postgres.outputs.connection-uri }}
58+
59+
- run: pytest -vv tests/
60+
env:
61+
CONNECTION_STR: service=${{ steps.postgres.outputs.service-name }}
62+
```
63+
64+
## Recipes
65+
66+
#### Create a new user w/ database via CLI
67+
68+
```yaml
69+
steps:
70+
- uses: ikalnytskyi/action-setup-postgres@v3
71+
72+
- run: |
73+
createuser myuser
74+
createdb --owner myuser mydatabase
75+
psql -c "ALTER USER myuser WITH PASSWORD 'mypassword'"
76+
77+
env:
78+
# This activates connection parameters for the superuser created by
79+
# the action in the step above. It's mandatory to set this before using
80+
# createuser/psql/etc tools.
81+
#
82+
# The service name is the same as the username (i.e. 'postgres') but
83+
# it's recommended to use action's output to get the name in order to
84+
# be forward compatible.
85+
PGSERVICE: ${{ steps.postgres.outputs.service-name }}
86+
shell: bash
87+
```
88+
89+
#### Create a new user w/ database via psycopg
90+
91+
```yaml
92+
steps:
93+
- uses: ikalnytskyi/action-setup-postgres@v3
94+
```
95+
96+
```python
97+
import psycopg
98+
99+
# 'postgres' is the username here, but it's recommended to use the
100+
# action's 'service-name' output parameter here.
101+
connection = psycopg.connect("service=postgres")
102+
103+
# CREATE/DROP USER statements don't work within transactions, and with
104+
# autocommit disabled transactions are created by psycopg automatically.
105+
connection.autocommit = True
106+
connection.execute(f"CREATE USER myuser WITH PASSWORD 'mypassword'")
107+
connection.execute(f"CREATE DATABASE mydatabase WITH OWNER 'myuser'")
57108
```
58109

59110
## Rationale

action.yml

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ inputs:
2424
outputs:
2525
connection-uri:
2626
description: The connection URI to connect to PostgreSQL.
27-
value: ${{ steps.connection-uri.outputs.value }}
27+
value: ${{ steps.set-outputs.outputs.connection-uri }}
28+
service-name:
29+
description: The service name with connection parameters.
30+
value: ${{ steps.set-outputs.outputs.service-name }}
2831
runs:
2932
using: composite
3033
steps:
@@ -71,21 +74,27 @@ runs:
7174
echo "port = ${{ inputs.port }}" >> "$PGDATA/postgresql.conf"
7275
pg_ctl start
7376
74-
# Set environment variables for PostgreSQL client applications [1] such
75-
# as 'psql' or 'createuser'.
77+
# Save required connection parameters for created superuser to the
78+
# connection service file [1]. This allows using these connection
79+
# parameters by setting 'PGSERVICE' environment variable or by
80+
# requesting them via connection string.
7681
#
77-
# PGHOST is required for Linux/macOS because we turned off unix sockets
78-
# and they use them by default.
82+
# HOST is required for Linux/macOS because these OS-es default to unix
83+
# sockets but we turned them off.
7984
#
80-
# PGPORT, PGUSER, PGPASSWORD and PGDATABASE are required because they
81-
# could be parametrized via action input parameters.
85+
# PORT, USER, PASSWORD and DBNAME are required because they could be
86+
# parametrized via action input parameters.
8287
#
83-
# [1] https://www.postgresql.org/docs/15/reference-client.html
84-
echo "PGHOST=localhost" >> $GITHUB_ENV
85-
echo "PGPORT=${{ inputs.port }}" >> $GITHUB_ENV
86-
echo "PGUSER=${{ inputs.username }}" >> $GITHUB_ENV
87-
echo "PGPASSWORD=${{ inputs.password }}" >> $GITHUB_ENV
88-
echo "PGDATABASE=${{ inputs.database }}" >> $GITHUB_ENV
88+
# [1] https://www.postgresql.org/docs/15/libpq-pgservice.html
89+
cat <<EOF > "$PGDATA/pg_service.conf"
90+
[${{ inputs.username }}]
91+
host=localhost
92+
port=${{ inputs.port }}
93+
user=${{ inputs.username }}
94+
password=${{ inputs.password }}
95+
dbname=${{ inputs.database }}
96+
EOF
97+
echo "PGSERVICEFILE=$PGDATA/pg_service.conf" >> $GITHUB_ENV
8998
shell: bash
9099

91100
- name: Setup PostgreSQL database
@@ -97,11 +106,15 @@ runs:
97106
if [ "${{ inputs.database }}" != "postgres" ]; then
98107
createdb -O "${{ inputs.username }}" "${{ inputs.database }}"
99108
fi
109+
env:
110+
PGSERVICE: ${{ inputs.username }}
100111
shell: bash
101112

102-
- name: Expose connection URI
113+
- name: Set action outputs
103114
run: |
104-
CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{inputs.port}}/${{ inputs.database }}"
105-
echo "value=$CONNECTION_URI" >> $GITHUB_OUTPUT
115+
CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{ inputs.port }}/${{ inputs.database }}"
116+
117+
echo "connection-uri=$CONNECTION_URI" >> $GITHUB_OUTPUT
118+
echo "service-name=${{ inputs.username }}" >> $GITHUB_OUTPUT
106119
shell: bash
107-
id: connection-uri
120+
id: set-outputs

test_action.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ def connection_uri() -> str:
2121
return connection_uri
2222

2323

24+
@pytest.fixture(scope="function")
25+
def service_name() -> str:
26+
"""Read and return connection URI from environment."""
27+
28+
service_name = os.getenv("SERVICE_NAME")
29+
if service_name is None:
30+
pytest.fail("SERVICE_NAME: environment variable is not set")
31+
return service_name
32+
33+
2434
@pytest.fixture(scope="function")
2535
def connection_factory() -> ConnectionFactory:
2636
"""Return 'psycopg.Connection' factory."""
@@ -30,19 +40,32 @@ def factory(connection_uri: str) -> psycopg.Connection:
3040
return factory
3141

3242

33-
@pytest.fixture(scope="function")
34-
def connection(connection_uri: str, connection_factory: ConnectionFactory) -> psycopg.Connection:
43+
@pytest.fixture(scope="function", params=["uri", "kv-string"])
44+
def connection(
45+
request: pytest.FixtureRequest,
46+
connection_factory: ConnectionFactory,
47+
connection_uri: str,
48+
service_name: str,
49+
) -> psycopg.Connection:
3550
"""Return 'psycopg.Connection' for connection URI set in environment."""
3651

37-
return connection_factory(connection_uri)
52+
if request.param == "uri":
53+
return connection_factory(connection_uri)
54+
elif request.param == "kv-string":
55+
return connection_factory(f"service={service_name}")
56+
raise RuntimeError("f{request.param}: unknown value")
3857

3958

40-
def test_connection_uri():
59+
def test_connection_uri(connection_uri):
4160
"""Test that CONNECTION_URI matches EXPECTED_CONNECTION_URI."""
4261

43-
connection_uri = os.getenv("CONNECTION_URI")
44-
expected_connection_uri = os.getenv("EXPECTED_CONNECTION_URI")
45-
assert connection_uri == expected_connection_uri
62+
assert connection_uri == os.getenv("EXPECTED_CONNECTION_URI")
63+
64+
65+
def test_service_name(service_name):
66+
"""Test that SERVICE_NAME matches EXPECTED_SERVICE_NAME."""
67+
68+
assert service_name == os.getenv("EXPECTED_SERVICE_NAME")
4669

4770

4871
def test_server_encoding(connection: psycopg.Connection):
@@ -146,9 +169,18 @@ def test_user_create_drop_user(
146169
connection.execute(f"DROP USER {username}")
147170

148171

149-
def test_client_applications(connection_factory: ConnectionFactory, connection_uri: str):
172+
def test_client_applications(
173+
connection_factory: ConnectionFactory,
174+
service_name: str,
175+
connection_uri: str,
176+
monkeypatch: pytest.MonkeyPatch,
177+
):
150178
"""Test that PostgreSQL client applications can be used."""
151179

180+
# Request connection parameters from the connection service file prepared
181+
# by our action.
182+
monkeypatch.setenv("PGSERVICE", service_name)
183+
152184
username = "us3rname"
153185
password = "passw0rd"
154186
database = "databas3"

0 commit comments

Comments
 (0)