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 diff --git a/src/modules/services/postgres.nix b/src/modules/services/postgres.nix index ddc3771d4..c42ce00b9 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 @@ -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 '' @@ -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 @@ -107,7 +133,7 @@ let # Setup config cp ${configFile} "$PGDATA/postgresql.conf" - + # Setup pg_hba.conf ${setupPgHbaFileScript} @@ -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 = [ ]; 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) +);