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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ jobs:
python3 -m pytest -vv test_action.py
env:
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
EXPECTED_CONNECTION_URI: postgresql://postgres:postgres@localhost:5432/postgres
EXPECTED_SERVICE_NAME: postgres
shell: bash

parametrized:
Expand Down Expand Up @@ -59,5 +61,7 @@ jobs:
python3 -m pytest -vv test_action.py
env:
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order
EXPECTED_SERVICE_NAME: yoda
shell: bash
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ key features:
| Username | `postgres` |
| Password | `postgres` |
| Database | `postgres` |
| Service | `postgres` |

#### User permissions

Expand Down Expand Up @@ -53,7 +54,57 @@ steps:

- run: pytest -vv tests/
env:
DATABASE_URI: ${{ steps.postgres.outputs.connection-uri }}
CONNECTION_STR: ${{ steps.postgres.outputs.connection-uri }}

- run: pytest -vv tests/
env:
CONNECTION_STR: service=${{ steps.postgres.outputs.service-name }}
```

## Recipes

#### Create a new user w/ database via CLI

```yaml
steps:
- uses: ikalnytskyi/action-setup-postgres@v3

- run: |
createuser myuser
createdb --owner myuser mydatabase
psql -c "ALTER USER myuser WITH PASSWORD 'mypassword'"

env:
# This activates connection parameters for the superuser created by
# the action in the step above. It's mandatory to set this before using
# createuser/psql/etc tools.
#
# The service name is the same as the username (i.e. 'postgres') but
# it's recommended to use action's output to get the name in order to
# be forward compatible.
PGSERVICE: ${{ steps.postgres.outputs.service-name }}
shell: bash
```

#### Create a new user w/ database via psycopg

```yaml
steps:
- uses: ikalnytskyi/action-setup-postgres@v3
```

```python
import psycopg

# 'postgres' is the username here, but it's recommended to use the
# action's 'service-name' output parameter here.
connection = psycopg.connect("service=postgres")

# CREATE/DROP USER statements don't work within transactions, and with
# autocommit disabled transactions are created by psycopg automatically.
connection.autocommit = True
connection.execute(f"CREATE USER myuser WITH PASSWORD 'mypassword'")
connection.execute(f"CREATE DATABASE mydatabase WITH OWNER 'myuser'")
```

## Rationale
Expand Down
47 changes: 30 additions & 17 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ inputs:
outputs:
connection-uri:
description: The connection URI to connect to PostgreSQL.
value: ${{ steps.connection-uri.outputs.value }}
value: ${{ steps.set-outputs.outputs.connection-uri }}
service-name:
description: The service name with connection parameters.
value: ${{ steps.set-outputs.outputs.service-name }}
runs:
using: composite
steps:
Expand Down Expand Up @@ -71,21 +74,27 @@ runs:
echo "port = ${{ inputs.port }}" >> "$PGDATA/postgresql.conf"
pg_ctl start

# Set environment variables for PostgreSQL client applications [1] such
# as 'psql' or 'createuser'.
# Save required connection parameters for created superuser to the
# connection service file [1]. This allows using these connection
# parameters by setting 'PGSERVICE' environment variable or by
# requesting them via connection string.
#
# PGHOST is required for Linux/macOS because we turned off unix sockets
# and they use them by default.
# HOST is required for Linux/macOS because these OS-es default to unix
# sockets but we turned them off.
#
# PGPORT, PGUSER, PGPASSWORD and PGDATABASE are required because they
# could be parametrized via action input parameters.
# PORT, USER, PASSWORD and DBNAME are required because they could be
# parametrized via action input parameters.
#
# [1] https://www.postgresql.org/docs/15/reference-client.html
echo "PGHOST=localhost" >> $GITHUB_ENV
echo "PGPORT=${{ inputs.port }}" >> $GITHUB_ENV
echo "PGUSER=${{ inputs.username }}" >> $GITHUB_ENV
echo "PGPASSWORD=${{ inputs.password }}" >> $GITHUB_ENV
echo "PGDATABASE=${{ inputs.database }}" >> $GITHUB_ENV
# [1] https://www.postgresql.org/docs/15/libpq-pgservice.html
cat <<EOF > "$PGDATA/pg_service.conf"
[${{ inputs.username }}]
host=localhost
port=${{ inputs.port }}
user=${{ inputs.username }}
password=${{ inputs.password }}
dbname=${{ inputs.database }}
EOF
echo "PGSERVICEFILE=$PGDATA/pg_service.conf" >> $GITHUB_ENV
shell: bash

- name: Setup PostgreSQL database
Expand All @@ -97,11 +106,15 @@ runs:
if [ "${{ inputs.database }}" != "postgres" ]; then
createdb -O "${{ inputs.username }}" "${{ inputs.database }}"
fi
env:
PGSERVICE: ${{ inputs.username }}
shell: bash

- name: Expose connection URI
- name: Set action outputs
run: |
CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{inputs.port}}/${{ inputs.database }}"
echo "value=$CONNECTION_URI" >> $GITHUB_OUTPUT
CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{ inputs.port }}/${{ inputs.database }}"

echo "connection-uri=$CONNECTION_URI" >> $GITHUB_OUTPUT
echo "service-name=${{ inputs.username }}" >> $GITHUB_OUTPUT
shell: bash
id: connection-uri
id: set-outputs
48 changes: 40 additions & 8 deletions test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ def connection_uri() -> str:
return connection_uri


@pytest.fixture(scope="function")
def service_name() -> str:
"""Read and return connection URI from environment."""

service_name = os.getenv("SERVICE_NAME")
if service_name is None:
pytest.fail("SERVICE_NAME: environment variable is not set")
return service_name


@pytest.fixture(scope="function")
def connection_factory() -> ConnectionFactory:
"""Return 'psycopg.Connection' factory."""
Expand All @@ -30,19 +40,32 @@ def factory(connection_uri: str) -> psycopg.Connection:
return factory


@pytest.fixture(scope="function")
def connection(connection_uri: str, connection_factory: ConnectionFactory) -> psycopg.Connection:
@pytest.fixture(scope="function", params=["uri", "kv-string"])
def connection(
request: pytest.FixtureRequest,
connection_factory: ConnectionFactory,
connection_uri: str,
service_name: str,
) -> psycopg.Connection:
"""Return 'psycopg.Connection' for connection URI set in environment."""

return connection_factory(connection_uri)
if request.param == "uri":
return connection_factory(connection_uri)
elif request.param == "kv-string":
return connection_factory(f"service={service_name}")
raise RuntimeError("f{request.param}: unknown value")


def test_connection_uri():
def test_connection_uri(connection_uri):
"""Test that CONNECTION_URI matches EXPECTED_CONNECTION_URI."""

connection_uri = os.getenv("CONNECTION_URI")
expected_connection_uri = os.getenv("EXPECTED_CONNECTION_URI")
assert connection_uri == expected_connection_uri
assert connection_uri == os.getenv("EXPECTED_CONNECTION_URI")


def test_service_name(service_name):
"""Test that SERVICE_NAME matches EXPECTED_SERVICE_NAME."""

assert service_name == os.getenv("EXPECTED_SERVICE_NAME")


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


def test_client_applications(connection_factory: ConnectionFactory, connection_uri: str):
def test_client_applications(
connection_factory: ConnectionFactory,
service_name: str,
connection_uri: str,
monkeypatch: pytest.MonkeyPatch,
):
"""Test that PostgreSQL client applications can be used."""

# Request connection parameters from the connection service file prepared
# by our action.
monkeypatch.setenv("PGSERVICE", service_name)

username = "us3rname"
password = "passw0rd"
database = "databas3"
Expand Down