From 1fc316b98d671791179f11fb4f2e2d9830d6efe5 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Fri, 13 Mar 2026 17:23:22 +0000 Subject: [PATCH] feat: support changing owner, granting create on public --- README.md | 7 ++- ensure_role_and_database_exists.c | 97 ++++++++++++++++++++----------- ephemeral-postgres-config.sh | 4 ++ start-ephemeral-postgres.sh | 25 +++++++- 4 files changed, 93 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 17f7694..909cf98 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A bash script that starts an ephemeral postgres locally in docker for **developm By default, **Data is destroyed** between runs, and on Linux the data is stored on a `tmpfs` (ramdisk) for faster startup. -For persistent data, configure `EPHEMERAL_POSTGRES_DATA_DIR` to be a path you wish to +For persistent data, configure `EPHEMERAL_POSTGRES_DATA_DIR` to be a path you wish to store the data on your host machine. The container loads a `ClientAuthentication` hook `ensure_role_and_database_exists` that @@ -47,6 +47,9 @@ start-ephemeral-postgres.sh - `POSTGRES_HOST_AUTH_METHOD=trust` - could be `scram-sha-256` / `md5` / etc - `POSTGRES_ROLE_ATTRIBUTES='LOGIN CREATEDB'` - could be `SUPERUSER` / `CREATEROLE BYPASSRLS` / etc - `POSTGRES_EXTENSIONS=` - could be `postgis ltree` / etc +- `POSTGRES_DATABASE_OWNER=` - leave unset to default to the connecting user +- `POSTGRES_GRANT_PUBLIC_SCHEMA_CREATE=` - set to `true` to use pre-v15 default schema privileges + - `EPHEMERAL_POSTGRES_FORCE_BUILD=0` - force building the docker image locally instead of pulling a prebuilt image - `EPHEMERAL_POSTGRES_AUTO_UPDATE=1` - whether to automatically check for updates to `ephemeral-postgres` - `EPHEMERAL_POSTGRES_DATA_DIR=` - when empty, use a tmpfs / ram disk, otherwise a path to bind mount to the postgres data directory @@ -64,7 +67,7 @@ docker exec -it postgres psql -U any_username any_database_name ## Development -You can use [bear](https://github.com/rizsotto/Bear) to generate a `compile_commands.json` which is +You can use [bear](https://github.com/rizsotto/Bear) to generate a `compile_commands.json` which is understood by IDE's like CLion. ```shell diff --git a/ensure_role_and_database_exists.c b/ensure_role_and_database_exists.c index b48662a..e57558a 100644 --- a/ensure_role_and_database_exists.c +++ b/ensure_role_and_database_exists.c @@ -2,11 +2,12 @@ #include "fmgr.h" #include "libpq/auth.h" -#include "executor/spi.h" #include "miscadmin.h" PG_MODULE_MAGIC; +#define INTERNAL_MARKER "ephemeral_pg.internal=true" + static ClientAuthentication_hook_type original_client_auth_hook = NULL; static void execute_command(const char *cmd) { @@ -30,6 +31,11 @@ static void ensure_role_and_database_exists(Port *port, int status) { original_client_auth_hook(port, status); } + // Skip if this is an internal connection from our own psql commands. + if (port->cmdline_options && strstr(port->cmdline_options, INTERNAL_MARKER)) { + return; + } + if (!postgres_user) { ereport(ERROR, errmsg("POSTGRES_USER environment variable is not set.")); } @@ -38,43 +44,64 @@ static void ensure_role_and_database_exists(Port *port, int status) { ereport(ERROR, errmsg("POSTGRES_ROLE_ATTRIBUTES environment variable is not set.")); } - // don't infinitely recurse when connecting as superuser - if (strcmp(port->user_name, postgres_user) == 0 && strcmp(port->database_name, postgres_user) == 0) { - return; - } - - elog(LOG, "handling connection for username '%s' to database '%s'", port->user_name, port->database_name); - - elog(LOG, "ensuring user_name '%s' exists with attributes '%s'", port->user_name, role_attributes); - if (asprintf(&cmd, - "echo \"SELECT 'CREATE ROLE %s WITH %s' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '%s')\\gexec\" | psql -U %s -d %s", - port->user_name, - role_attributes, - port->user_name, - postgres_user, - postgres_user - ) < 0) { - ereport(ERROR, errmsg("failed to allocate command string")); + // if the requested user isn't the postgres user, ensure it exists + if (strcmp(port->user_name, postgres_user) != 0) { + elog(LOG, "handling connection for username '%s' to database '%s'", port->user_name, port->database_name); + + elog(LOG, "ensuring user_name '%s' exists with attributes '%s'", port->user_name, role_attributes); + if (asprintf(&cmd, + "echo \"SELECT 'CREATE ROLE %s WITH %s' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '%s')\\gexec\" | PGOPTIONS='-c ephemeral_pg.internal=true' psql -U %s -d %s", + port->user_name, + role_attributes, + port->user_name, + postgres_user, + postgres_user + ) < 0) { + ereport(ERROR, errmsg("failed to allocate command string")); + } + + execute_command(cmd); + free(cmd); } - execute_command(cmd); - free(cmd); - - elog(LOG, "ensuring database '%s' exists", port->database_name); - - if (asprintf(&cmd, - "echo \"SELECT 'CREATE DATABASE %s WITH OWNER = %s' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '%s')\\gexec\" | psql -U %s -d %s", - port->database_name, - port->user_name, - port->database_name, - postgres_user, - postgres_user - ) < 0) { - ereport(ERROR, errmsg("failed to allocate command string")); + // if the requested database isn't the postgres user database, ensure it exists and set permissions + if (strcmp(port->database_name, postgres_user) != 0) { + const char *database_owner_env = getenv("POSTGRES_DATABASE_OWNER"); + const char *database_owner = (database_owner_env && database_owner_env[0] != '\0') ? database_owner_env : port->user_name; + + elog(LOG, "ensuring database '%s' exists with owner '%s'", port->database_name, database_owner); + + if (asprintf(&cmd, + "echo \"SELECT 'CREATE DATABASE %s WITH OWNER = %s' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '%s')\\gexec\" | PGOPTIONS='-c ephemeral_pg.internal=true' psql -U %s -d %s", + port->database_name, + database_owner, + port->database_name, + postgres_user, + postgres_user + ) < 0) { + ereport(ERROR, errmsg("failed to allocate command string")); + } + + execute_command(cmd); + free(cmd); + + if(strcmp(port->database_name, "template1") == 0 || strcmp(port->database_name, "template0") == 0 ){ + return; + } + + // if enabled, grant posgtes v14 and earlier style permissions of create on schema public, for apps that rely on it. + const char *grant_public = getenv("POSTGRES_GRANT_PUBLIC_SCHEMA_CREATE"); + if (grant_public != NULL && strcmp(grant_public, "true") == 0) { + elog(LOG, "granting CREATE ON SCHEMA public TO PUBLIC for database '%s'", port->database_name); + + if(asprintf(&cmd, "PGOPTIONS='-c ephemeral_pg.internal=true' psql -U %s -d %s -c \"GRANT CREATE ON SCHEMA public TO PUBLIC\"", postgres_user, port->database_name) < 0) { + ereport(ERROR, errmsg("failed to allocate command string")); + } + + execute_command(cmd); + free(cmd); + } } - - execute_command(cmd); - free(cmd); } void _PG_init(void) { diff --git a/ephemeral-postgres-config.sh b/ephemeral-postgres-config.sh index 33e50ff..1b5ba29 100755 --- a/ephemeral-postgres-config.sh +++ b/ephemeral-postgres-config.sh @@ -7,6 +7,8 @@ set -eo pipefail : "${POSTGRES_PASSWORD:=postgres}" : "${POSTGRES_HOST_AUTH_METHOD:=trust}" : "${POSTGRES_ROLE_ATTRIBUTES:=LOGIN CREATEDB}" +: "${POSTGRES_DATABASE_OWNER:=}" +: "${POSTGRES_GRANT_PUBLIC_SCHEMA_CREATE:=true}" : "${POSTGRES_EXTENSIONS:=}" : "${EPHEMERAL_POSTGRES_AUTO_UPDATE:=1}" @@ -25,6 +27,8 @@ export POSTGRES_USER export POSTGRES_PASSWORD export POSTGRES_HOST_AUTH_METHOD export POSTGRES_ROLE_ATTRIBUTES +export POSTGRES_DATABASE_OWNER +export POSTGRES_GRANT_PUBLIC_SCHEMA_CREATE export POSTGRES_EXTENSIONS export EPHEMERAL_POSTGRES_AUTO_UPDATE diff --git a/start-ephemeral-postgres.sh b/start-ephemeral-postgres.sh index 1484d33..e480e86 100755 --- a/start-ephemeral-postgres.sh +++ b/start-ephemeral-postgres.sh @@ -92,11 +92,14 @@ if [ -n "$EPHEMERAL_POSTGRES_DATA_DIR" ]; then fi else + # Postgres encounters permission issues when using the ram disk unless run as its default linux user + EPHEMERAL_POSTGRES_LINUX_USER='' + if [[ "$OSTYPE" =~ ^linux ]]; then echo "Using ram disk" EPHEMERAL_POSTGRES_DOCKER_RUN_ARGS+='--mount type=tmpfs,destination=/var/lib/postgresql' - # Postgres encounters permission issues when using the ram disk unless run as its default linux user - EPHEMERAL_POSTGRES_LINUX_USER='' + else + echo "Using default docker volume" fi fi @@ -110,12 +113,28 @@ docker run -d --name postgres $EPHEMERAL_POSTGRES_DOCKER_RUN_ARGS \ -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \ -e POSTGRES_HOST_AUTH_METHOD="${POSTGRES_HOST_AUTH_METHOD}" \ -e POSTGRES_ROLE_ATTRIBUTES="${POSTGRES_ROLE_ATTRIBUTES}" \ + -e POSTGRES_DATABASE_OWNER="${POSTGRES_DATABASE_OWNER}" \ + -e POSTGRES_GRANT_PUBLIC_SCHEMA_CREATE="${POSTGRES_GRANT_PUBLIC_SCHEMA_CREATE}" \ -p 5432:5432 "${IMAGE}" \ -c shared_buffers=256MB \ -c 'shared_preload_libraries=$libdir/ensure_role_and_database_exists' +WAIT_ATTEMPTS=0 while ! docker exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_USER" -c 'SELECT 1;' > /dev/null 2>&1; do - echo "Waiting for postgres to start..." + WAIT_ATTEMPTS=$((WAIT_ATTEMPTS + 1)) + + # Clear previous output (move cursor up past the log lines + header) + if [[ $WAIT_ATTEMPTS -gt 1 && -n "$PREV_LOG_LINES" ]]; then + for ((i = 0; i < PREV_LOG_LINES + 1; i++)); do + printf '\033[1A\033[2K' + done + fi + + echo "Waiting for postgres to start... (attempt $WAIT_ATTEMPTS)" + LOGS=$(docker logs --tail 10 postgres 2>&1 || true) + PREV_LOG_LINES=$(echo "$LOGS" | wc -l) + echo "$LOGS" + sleep 1 done