diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix index 3bedfe96a1805..0c57398d4f135 100644 --- a/nixos/modules/services/databases/postgresql.nix +++ b/nixos/modules/services/databases/postgresql.nix @@ -28,8 +28,86 @@ in ###### interface - options = { - + options = let + databaseOpts = { + options = { + name = mkOption { + type = types.str; + description = '' + The name of the database to ensure. + ''; + example = "gitlab"; + }; + owner = mkOption { + type = with types; nullOr str; + default = null; + description = '' + The name of the database owner. + + The user will be created if it doesn't already + exist. The owner of an existing database will be + changed if it's changed here, and all objects in the + database owned by the previous owner will be + reassigned to the new one. + + See + Creating a Database, + REASSIGN OWNED + and + ALTER DATABASE + for more details. + ''; + }; + extensions = mkOption { + type = with types; listOf str; + default = []; + description = '' + Extensions to add to the database. + + Extensions will be added to existing databases, but + not removed if they're removed from the list. + + See + + CREATE EXTENSION for more details. + ''; + example = [ "pg_trgm" "hstore" ]; + }; + }; + }; + userOpts = { + options = { + name = mkOption { + type = types.str; + description = '' + Name of the user to ensure. + ''; + }; + ensurePermissions = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Permissions to ensure for the user, specified as an attribute set. + The attribute names specify the database and tables to grant the permissions for. + The attribute values specify the permissions to grant. You may specify one or + multiple comma-separated SQL privileges here. + + For more information on how to specify the target + and on which privileges exist, see the + GRANT syntax. + The attributes are used as GRANT ''${attrName} ON ''${attrValue}. + ''; + example = literalExample '' + { + "DATABASE nextcloud" = "ALL PRIVILEGES"; + "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES"; + } + ''; + }; + }; + }; + in + { services.postgresql = { enable = mkOption { @@ -97,13 +175,23 @@ in }; ensureDatabases = mkOption { - type = types.listOf types.str; + type = with types; + listOf (coercedTo str + (name: { inherit name; }) + (submodule databaseOpts)); default = []; description = '' - Ensures that the specified databases exist. - This option will never delete existing databases, especially not when the value of this - option is changed. This means that databases created once through this option or - otherwise have to be removed manually. + Ensure the existence of the listed databases with their + respective owner and extensions. + + This option will never delete existing databases; if the + name of a database is changed here, a new database with that + name will be created and the old one will remain. Databases + created once through this option or otherwise have to be + removed manually. + + Extensions will be added to existing databases, but not + removed if they're removed from the list. ''; example = [ "gitea" @@ -112,37 +200,7 @@ in }; ensureUsers = mkOption { - type = types.listOf (types.submodule { - options = { - name = mkOption { - type = types.str; - description = '' - Name of the user to ensure. - ''; - }; - ensurePermissions = mkOption { - type = types.attrsOf types.str; - default = {}; - description = '' - Permissions to ensure for the user, specified as an attribute set. - The attribute names specify the database and tables to grant the permissions for. - The attribute values specify the permissions to grant. You may specify one or - multiple comma-separated SQL privileges here. - - For more information on how to specify the target - and on which privileges exist, see the - GRANT syntax. - The attributes are used as GRANT ''${attrName} ON ''${attrValue}. - ''; - example = literalExample '' - { - "DATABASE nextcloud" = "ALL PRIVILEGES"; - "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES"; - } - ''; - }; - }; - }); + type = with types; listOf (submodule userOpts); default = []; description = '' Ensures that the specified users exist and have at least the ensured permissions. @@ -320,6 +378,7 @@ in # Wait for PostgreSQL to be ready to accept connections. postStart = '' + set -eu PSQL="${pkgs.sudo}/bin/sudo -u ${cfg.superUser} psql --port=${toString cfg.port}" while ! $PSQL -d postgres -c "" 2> /dev/null; do @@ -333,18 +392,51 @@ in ''} rm -f "${cfg.dataDir}/.first_startup" fi - '' + optionalString (cfg.ensureDatabases != []) '' - ${concatMapStrings (database: '' - $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${database}"' - '') cfg.ensureDatabases} - '' + '' - ${concatMapStrings (user: '' - $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc "CREATE USER ${user.name}" + '' + + concatMapStrings (user: '' + $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user}'" | grep -q 1 || $PSQL -tAc "CREATE USER ${user}" + '') (unique (concatMap (a: if a ? owner then if a.owner != null then [a.owner] else [] else [a.name]) (cfg.ensureUsers ++ cfg.ensureDatabases))) + + concatMapStrings (db: '' + database_exists=$($PSQL -tAc "SELECT 'true' FROM pg_database WHERE datname = '${db.name}'") + if [[ "$database_exists" != "true" ]]; then + $PSQL -tAc 'CREATE DATABASE "${db.name}" ${optionalString (db.owner != null) ''OWNER "${db.owner}"''}' + fi + + echo "${db.name}" >> "${cfg.dataDir}/databases" + + ${optionalString (db.owner != null) '' + current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${db.name}'") + if [[ "$current_owner" != "${db.owner}" ]]; then + $PSQL -tAc 'ALTER DATABASE "${db.name}" OWNER TO "${db.owner}"' + if [[ -e "${cfg.dataDir}/.reassigning_${db.name}" ]]; then + echo "Reassigning ownership of ${db.name} to ${db.owner} failed on last boot. Failing..." + exit 1 + fi + touch "${cfg.dataDir}/.reassigning_${db.name}" + $PSQL "${db.name}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${db.owner}\"" + rm "${cfg.dataDir}/.reassigning_${db.name}" + fi + ''} + '' + + concatMapStrings (extension: '' + $PSQL '${db.name}' -tAc "CREATE EXTENSION IF NOT EXISTS ${extension}" + '') db.extensions + ) cfg.ensureDatabases + + '' + touch "${cfg.dataDir}/databases" + if [[ -f "${cfg.dataDir}/existing_databases" ]]; then + for db in $(grep -vFxf "${cfg.dataDir}/databases" "${cfg.dataDir}/existing_databases" || echo ""); do + echo "Dropping database $db!" + $PSQL -tAc "DROP DATABASE \"$db\"" + done + fi + mv "${cfg.dataDir}/databases" "${cfg.dataDir}/existing_databases" + '' + + concatMapStrings (user: '' ${concatStringsSep "\n" (mapAttrsToList (database: permission: '' $PSQL -tAc 'GRANT ${permission} ON ${database} TO ${user.name}' '') user.ensurePermissions)} - '') cfg.ensureUsers} - ''; + '') cfg.ensureUsers; unitConfig.RequiresMountsFor = "${cfg.dataDir}"; }; diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix index 07ea9c4584371..5f463d8950130 100644 --- a/nixos/modules/services/misc/gitlab.nix +++ b/nixos/modules/services/misc/gitlab.nix @@ -602,28 +602,12 @@ in { # We use postgres as the main data store. services.postgresql = optionalAttrs databaseActuallyCreateLocally { enable = true; - ensureUsers = singleton { name = cfg.databaseUsername; }; + ensureDatabases = singleton { + name = cfg.databaseName; + owner = cfg.databaseUsername; + extensions = [ "pg_trgm" ]; + }; }; - # The postgresql module doesn't currently support concepts like - # objects owners and extensions; for now we tack on what's needed - # here. - systemd.services.postgresql.postStart = mkAfter (optionalString databaseActuallyCreateLocally '' - set -eu - - $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"' - current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'") - if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then - $PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"' - if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then - echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..." - exit 1 - fi - touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" - $PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\"" - rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" - fi - $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm" - ''); # Use postfix to send out mails. services.postfix.enable = mkDefault true;