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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
97 changes: 62 additions & 35 deletions ensure_role_and_database_exists.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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."));
}
Expand All @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions ephemeral-postgres-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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
Expand Down
25 changes: 22 additions & 3 deletions start-ephemeral-postgres.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading