From b97b992360df5bc76e9ad140c6d832c4b1d42f7e Mon Sep 17 00:00:00 2001 From: ppenguin Date: Sun, 1 Sep 2024 14:25:18 +0200 Subject: [PATCH 1/4] postgres: uppercase existing sql keywords --- src/modules/services/postgres.nix | 73 +++++++++++++++++++------------ 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/modules/services/postgres.nix b/src/modules/services/postgres.nix index ddc3771d4..5ae807995 100644 --- a/src/modules/services/postgres.nix +++ b/src/modules/services/postgres.nix @@ -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 @@ -20,20 +24,21 @@ let else cfg.package; 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}';" | \ + 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 + echo 'CREATE DATABASE "${database.name}";' | psql --dbname postgres ${lib.optionalString (database.schema != null) '' echo "Applying database schema on ${database.name}" @@ -54,7 +59,7 @@ let echo "ERROR: Could not determine how to apply schema with ${database.schema}" exit 1 fi - ''} + ''} fi '') cfg.initialDatabases) @@ -66,27 +71,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 @@ -107,7 +112,7 @@ let # Setup config cp ${configFile} "$PGDATA/postgresql.conf" - + # Setup pg_hba.conf ${setupPgHbaFileScript} @@ -256,6 +261,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 = [ ]; From 99ccf470f731de298338d966249bfb3ebab37491 Mon Sep 17 00:00:00 2001 From: ppenguin Date: Sun, 1 Sep 2024 16:09:43 +0200 Subject: [PATCH 2/4] add user/pass functionality to postgres db init --- src/modules/services/postgres.nix | 89 +++++++++++++++++++------------ 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/src/modules/services/postgres.nix b/src/modules/services/postgres.nix index 5ae807995..c42ce00b9 100644 --- a/src/modules/services/postgres.nix +++ b/src/modules/services/postgres.nix @@ -23,45 +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 (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 + (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 '' From 39e166357236260fd5b59f0e6b4de84b7e5cd88d Mon Sep 17 00:00:00 2001 From: ppenguin Date: Sun, 1 Sep 2024 16:30:20 +0200 Subject: [PATCH 3/4] add option documentation --- docs/reference/options.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/reference/options.md b/docs/reference/options.md index cc070281c..809bfff51 100644 --- a/docs/reference/options.md +++ b/docs/reference/options.md @@ -34784,6 +34784,8 @@ list of (submodule) [ { name = "foodatabase"; + user = "ufoo"; + pass = "barpaz"; schema = ./foodatabase.sql; } { name = "bardatabase"; } @@ -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 From 350a325786647cd7984b2ccde5105719d5267553 Mon Sep 17 00:00:00 2001 From: ppenguin Date: Fri, 6 Sep 2024 12:32:09 +0200 Subject: [PATCH 4/4] postgresql: add test for postgresql-customdbuser --- tests/postgresql-customdbuser/.test.sh | 30 ++++++++++++++++++++ tests/postgresql-customdbuser/devenv.nix | 19 +++++++++++++ tests/postgresql-customdbuser/testinitdb.sql | 6 ++++ 3 files changed, 55 insertions(+) create mode 100755 tests/postgresql-customdbuser/.test.sh create mode 100644 tests/postgresql-customdbuser/devenv.nix create mode 100644 tests/postgresql-customdbuser/testinitdb.sql diff --git a/tests/postgresql-customdbuser/.test.sh b/tests/postgresql-customdbuser/.test.sh new file mode 100755 index 000000000..4849d56f7 --- /dev/null +++ b/tests/postgresql-customdbuser/.test.sh @@ -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 diff --git a/tests/postgresql-customdbuser/devenv.nix b/tests/postgresql-customdbuser/devenv.nix new file mode 100644 index 000000000..f7bd10a1f --- /dev/null +++ b/tests/postgresql-customdbuser/devenv.nix @@ -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 + } + ]; + }; +} diff --git a/tests/postgresql-customdbuser/testinitdb.sql b/tests/postgresql-customdbuser/testinitdb.sql new file mode 100644 index 000000000..dca7191b9 --- /dev/null +++ b/tests/postgresql-customdbuser/testinitdb.sql @@ -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) +);