Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve services.postgres db init #1408

Merged
merged 4 commits into from
Sep 11, 2024
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
34 changes: 34 additions & 0 deletions docs/reference/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -34784,6 +34784,8 @@ list of (submodule)
[
{
name = "foodatabase";
user = "ufoo";
pass = "barpaz";
schema = ./foodatabase.sql;
}
{ name = "bardatabase"; }
Expand All @@ -34804,6 +34806,38 @@ The name of the database to create.



*Type:*
string

*Declared by:*
- [https://github.com/cachix/devenv/blob/main/src/modules/services/postgres.nix](https://github.com/cachix/devenv/blob/main/src/modules/services/postgres.nix)



## services.postgres.initialDatabases.\*.user



The user who owns the database.



*Type:*
string

*Declared by:*
- [https://github.com/cachix/devenv/blob/main/src/modules/services/postgres.nix](https://github.com/cachix/devenv/blob/main/src/modules/services/postgres.nix)



## services.postgres.initialDatabases.\*.pass



The password of the user who owns the database.



*Type:*
string

Expand Down
156 changes: 98 additions & 58 deletions src/modules/services/postgres.nix
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
{ pkgs, lib, config, ... }:

{ pkgs
, lib
, config
, ...
}:
let
cfg = config.services.postgres;
types = lib.types;
inherit (lib) types;

q = lib.escapeShellArg;

runtimeDir = "${config.env.DEVENV_RUNTIME}/postgres";

postgresPkg =
if cfg.extensions != null then
if cfg.extensions != null
then
if builtins.hasAttr "withPackages" cfg.package
then cfg.package.withPackages cfg.extensions
else
Expand All @@ -19,44 +23,66 @@ let
''
else cfg.package;

# TODO: we can probably clean this up a lot by delegating more "if exists" stuff to psql (à la `DO $$...$$` below)
setupInitialDatabases =
if cfg.initialDatabases != [ ] then
if cfg.initialDatabases != [ ]
then
(lib.concatMapStrings
(database: ''
echo "Checking presence of database: ${database.name}"
# Create initial databases
dbAlreadyExists="$(
echo "SELECT 1 as exists FROM pg_database WHERE datname = '${database.name}';" | \
psql --dbname postgres | \
${pkgs.gnugrep}/bin/grep -c 'exists = "1"' || true
)"
echo $dbAlreadyExists
if [ 1 -ne "$dbAlreadyExists" ]; then
echo "Creating database: ${database.name}"
echo 'create database "${database.name}";' | psql --dbname postgres

${lib.optionalString (database.schema != null) ''
echo "Applying database schema on ${database.name}"
if [ -f "${database.schema}" ]
then
echo "Running file ${database.schema}"
${pkgs.gawk}/bin/awk 'NF' "${database.schema}" | psql --dbname ${database.name}
elif [ -d "${database.schema}" ]
then
# Read sql files in version order. Apply one file
# at a time to handle files where the last statement
# doesn't end in a ;.
ls -1v "${database.schema}"/*.sql | while read f ; do
echo "Applying sql file: $f"
${pkgs.gawk}/bin/awk 'NF' "$f" | psql --dbname ${database.name}
done
else
echo "ERROR: Could not determine how to apply schema with ${database.schema}"
exit 1
fi
(database:
let
psqlUserFlags =
if (database.user != null && database.pass != null)
then "--user ${database.user}"
else "";
in
''
echo "Checking presence of database: ${database.name}"
# Create initial databases
dbAlreadyExists="$(
echo "SELECT 1 AS exists FROM pg_database WHERE datname = '${database.name}';" | \
psql --dbname postgres | \
${pkgs.gnugrep}/bin/grep -c 'exists = "1"' || true
)"
echo $dbAlreadyExists
if [ 1 -ne "$dbAlreadyExists" ]; then
echo "Creating database: ${database.name}"
echo 'CREATE DATABASE "${database.name}";' | psql --dbname postgres
${lib.optionalString (database.schema != null && database.user != null && database.pass != null) ''
echo "Creating role ${database.user}..."
psql --dbname postgres <<'EOF'
DO $$
BEGIN
CREATE ROLE ${database.user} WITH LOGIN PASSWORD '${database.pass}';
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;
GRANT ALL PRIVILEGES ON DATABASE ${database.name} TO ${database.user};
\c ${database.name}
GRANT ALL PRIVILEGES ON SCHEMA public TO ${database.user};
EOF
''}
${lib.optionalString (database.schema != null) ''
echo "Applying database schema on ${database.name}"
if [ -f "${database.schema}" ]
then
echo "Running file ${database.schema}"
${pkgs.gawk}/bin/awk 'NF' "${database.schema}" | psql ${psqlUserFlags} --dbname ${database.name}
elif [ -d "${database.schema}" ]
then
# Read sql files in version order. Apply one file
# at a time to handle files where the last statement
# doesn't end in a ;.
ls -1v "${database.schema}"/*.sql | while read f ; do
echo "Applying sql file: $f"
${pkgs.gawk}/bin/awk 'NF' "$f" | psql ${psqlUserFlags} --dbname ${database.name}
done
else
echo "ERROR: Could not determine how to apply schema with ${database.schema}"
exit 1
fi
''}
fi
'')
fi
'')
cfg.initialDatabases)
else
lib.optionalString cfg.createDatabase ''
Expand All @@ -66,27 +92,27 @@ let
'';

runInitialScript =
if cfg.initialScript != null then
''
echo ${q cfg.initialScript} | psql --dbname postgres
''
else
"";
if cfg.initialScript != null
then ''
echo ${q cfg.initialScript} | psql --dbname postgres
''
else "";

toStr = value:
if true == value then
"yes"
else if false == value then
"no"
else if lib.isString value then
"'${lib.replaceStrings [ "'" ] [ "''" ] value}'"
else
toString value;

configFile = pkgs.writeText "postgresql.conf" (lib.concatStringsSep "\n"
(lib.mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));
if true == value
then "yes"
else if false == value
then "no"
else if lib.isString value
then "'${lib.replaceStrings ["'"] ["''"] value}'"
else toString value;

configFile =
pkgs.writeText "postgresql.conf" (lib.concatStringsSep "\n"
(lib.mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));
setupPgHbaFileScript =
if cfg.hbaConf != null then
if cfg.hbaConf != null
then
let
file = pkgs.writeText "pg_hba.conf" cfg.hbaConf;
in
Expand All @@ -107,7 +133,7 @@ let

# Setup config
cp ${configFile} "$PGDATA/postgresql.conf"

# Setup pg_hba.conf
${setupPgHbaFileScript}

Expand Down Expand Up @@ -256,6 +282,20 @@ in
an empty database is created.
'';
};
user = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Username of owner of the database (if null, the default $USER is used).
'';
};
pass = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Password of owner of the database (only takes effect if `user` is not `null`).
'';
};
};
});
default = [ ];
Expand Down
30 changes: 30 additions & 0 deletions tests/postgresql-customdbuser/.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
set -e

wait_for_port 2345
pg_isready -d template1

# negative check (whether error handling in the test is reliable)
psql \
--set ON_ERROR_STOP=on \
--username=notexists \
--dbname=testdb \
--echo-all \
-c '\dt' && {
echo "Problem with error handling!!!"
exit 1
}

# now check whether we can connect to our db as our new user and have permission to do stuff with the DB
psql \
--set ON_ERROR_STOP=on \
--username=testuser \
--dbname=testdb \
--echo-all \
--file=- <<'EOF'
\dt
SELECT * FROM supermasters;
INSERT INTO
supermasters (ip,nameserver,account)
VALUES ('10.100.9.99','dns.example.org','exampleaccount');
SELECT * FROM supermasters;
EOF
19 changes: 19 additions & 0 deletions tests/postgresql-customdbuser/devenv.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
services.postgres = {
enable = true;
listen_addresses = "localhost";
port = 2345;
# NOTE: use default for initialScript, which is:
# initialScript = ''
# CREATE USER postgres SUPERUSER;
# '';
initialDatabases = [
{
name = "testdb";
user = "testuser";
pass = "testuserpass";
schema = ./.; # *.sql in version order
}
];
};
}
6 changes: 6 additions & 0 deletions tests/postgresql-customdbuser/testinitdb.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS supermasters (
ip INET NOT NULL,
nameserver VARCHAR(255) NOT NULL,
account VARCHAR(40) NOT NULL,
PRIMARY KEY (ip, nameserver)
);
Loading