From 88ff92dcd0fea745fc27a868d9307dd0b8d1ed9c Mon Sep 17 00:00:00 2001 From: Ihor Kalnytskyi Date: Sun, 17 Jan 2021 14:29:05 +0200 Subject: [PATCH] Add basic implementation The 'setup-postgres' action has been developed by me for xsnippet/xsnippet-api [1] purpose where we want to run our tests on all major operating systems. Existing actions are built around Docker containers, and hence are available for Linux only. I figured that the work I have done there might be useful for someone else, and so I decided to create a standalone reusable action and publish it on GitHub Marketplace. [1] https://github.com/xsnippet/xsnippet-api/blob/40953d18b70e19d8535bf3aee88f28fe1407b36a/.github/actions/setup-postgres/action.yml --- .github/workflows/ci.yml | 62 ++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++++++ README.md | 52 +++++++++++++++++++++++++++ action.yml | 78 ++++++++++++++++++++++++++++++++++++++++ test_action.py | 65 +++++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 action.yml create mode 100644 test_action.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9145eac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: "0 8 * * *" + +jobs: + default: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - uses: actions/checkout@v2 + + - name: Run setup-postgres + uses: ./ + id: postgres + + - name: Run tests + run: | + python3 -m pip install --upgrade pip pytest psycopg + python3 -m pytest -vv test_action.py + env: + CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }} + EXPECTED_CONNECTION_URI: postgresql://postgres:postgres@localhost/postgres + shell: bash + + parametrized: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - uses: actions/checkout@v2 + + - name: Run setup-postgres + uses: ./ + with: + username: yoda + password: GrandMaster + database: jedi_order + id: postgres + + - name: Run tests + run: | + python3 -m pip install --upgrade pip pytest psycopg + python3 -m pytest -vv test_action.py + env: + CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }} + EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost/jedi_order + shell: bash diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8aab1c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ihor Kalnytskyi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a3da0b --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# setup-postgres + +This action sets up a PostgreSQL server for the rest of the job. Here are some +key features: + +* Runs on Linux, macOS and Windows runners. +* Adds PostgreSQL [binaries][1] (e.g. `psql`) to `PATH`. +* Uses PostgreSQL installed in [GitHub Actions Virtual Environments][2]. +* [Easy to check][3] that IT DOES NOT contain malicious code. + +[1]: https://www.postgresql.org/docs/current/reference-client.html +[2]: https://github.com/actions/virtual-environments +[3]: action.yml + +## Usage + +| Key | Value | +|----------|-----------------------------------------------------| +| URI | `postgresql://postgres:postgres@localhost/postgres` | +| Host | `localhost` | +| Port | `5432` | +| Username | `postgres` | +| Password | `postgres` | +| Database | `postgres` | + +#### Basic + +```yaml +steps: + - uses: ikalnytskyi/action-setup-postgres@v1 +``` + +#### Advanced + +```yaml +steps: + - uses: ikalnytskyi/action-setup-postgres@v1 + with: + username: ci + password: sw0rdfish + database: test + id: postgres + + - run: pytest -vv tests/ + env: + DATABASE_URI: ${{ steps.postgres.outputs.connection-uri }} +``` + +## License + +The scripts and documentation in this project are released under the +[MIT License](LICENSE). diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..eaf606e --- /dev/null +++ b/action.yml @@ -0,0 +1,78 @@ +name: Setup PostgreSQL for Linux/macOS/Windows +author: Ihor Kalnytskyi +description: Setup a preinstalled PostgreSQL server. +branding: + icon: database + color: purple +inputs: + username: + description: The username of the user to setup. + default: postgres + required: false + password: + description: The password of the user to setup. + default: postgres + required: false + database: + description: The database name to setup and grant permissions to created user. + default: postgres + required: false +outputs: + connection-uri: + description: The connection URI to connect to PostgreSQL. + value: ${{ steps.connection-uri.outputs.value }} +runs: + using: composite + steps: + - name: Prerequisites + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + echo "$(pg_config --bindir)" >> $GITHUB_PATH + elif [ "$RUNNER_OS" == "Windows" ]; then + echo "$PGBIN" >> $GITHUB_PATH + echo "PQ_LIB_DIR=$PGROOT\lib" >> $GITHUB_ENV + fi + shell: bash + + + - name: Setup and start PostgreSQL + run: | + export PGDATA="$RUNNER_TEMP/pgdata" + pg_ctl init + + # Forbid creating unix sockets since they are created by default in the + # directory we don't have permissions to. + echo "unix_socket_directories = ''" >> "$PGDATA/postgresql.conf" + pg_ctl start + + # Both PGHOST and PGUSER are used by PostgreSQL tooling such as 'psql' + # or 'createuser'. Since PostgreSQL data has been resetup, we cannot + # rely on defaults anymore. + # + # PGHOST is required for Linux and macOS since they default to unix + # sockets, and we have turned them off. + # + # PGUSER is required for Windows since default the tooling's default + # user is 'postgres', while 'pg_ctl init' creates one with the name of + # the current user. + echo "PGHOST=localhost" >> $GITHUB_ENV + echo "PGUSER=${USER:-$USERNAME}" >> $GITHUB_ENV + shell: bash + + - name: Setup PostgreSQL user and database + run: | + createuser --createdb ${{ inputs.username }} + + if [ "${{ inputs.database}}" != "postgres" ]; then + createdb -O ${{ inputs.username }} ${{ inputs.database }} + fi + + psql -c "ALTER USER ${{ inputs.username }} PASSWORD '${{ inputs.password }}';" ${{ inputs.database }} + shell: bash + + - name: Expose connection URI + run: | + CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost/${{ inputs.database }}" + echo ::set-output name=value::$CONNECTION_URI + shell: bash + id: connection-uri diff --git a/test_action.py b/test_action.py new file mode 100644 index 0000000..4f468a8 --- /dev/null +++ b/test_action.py @@ -0,0 +1,65 @@ +import typing as t +import os + +import psycopg +import pytest + + +@pytest.fixture(scope="function") +def connection_factory() -> t.Callable[[str], psycopg.Connection]: + def factory(connection_uri: str) -> psycopg.Connection: + return psycopg.connect(connection_uri) + return factory + + +@pytest.fixture(scope="function") +def connection(connection_factory) -> psycopg.Connection: + return connection_factory(os.getenv("CONNECTION_URI")) + + +def test_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 + + +def test_user_permissions(connection: psycopg.Connection): + """Test that a user can create databases but is not a superuser.""" + + with connection: + record = connection \ + .execute("SELECT usecreatedb, usesuper FROM pg_user WHERE usename = CURRENT_USER") \ + .fetchone() + assert record + + usecreatedb, usesuper = record + assert usecreatedb + assert not usesuper + + +def test_user_create_insert_select(connection: psycopg.Connection): + """Test that a user has CRUD permissions in a database.""" + + table_name = "test_setup_postgres" + + with connection, connection.transaction(force_rollback=True): + records = connection \ + .execute(f"CREATE TABLE {table_name}(eggs INTEGER, rice VARCHAR)") \ + .execute(f"INSERT INTO {table_name}(eggs, rice) VALUES (1, '42')") \ + .execute(f"SELECT * FROM {table_name}") \ + .fetchall() + assert records == [(1, "42")] + + +def test_user_create_drop_database(connection: psycopg.Connection): + """Test that a user has no permissions to create databases.""" + + # CREATE/DROP DATABASE statements don't work within transactions, and with + # autocommit disabled transactions are created by psycopg automatically. + connection.autocommit = True + + database_name = "foobar42" + connection.execute(f"CREATE DATABASE {database_name}") + connection.execute(f"DROP DATABASE {database_name}")