diff --git a/modules/services/postgresql/default.nix b/modules/services/postgresql/default.nix
index db5c02554..bcaf95e2b 100644
--- a/modules/services/postgresql/default.nix
+++ b/modules/services/postgresql/default.nix
@@ -3,69 +3,198 @@
with lib;
let
+
cfg = config.services.postgresql;
- postgresqlAndPlugins = pg:
- if cfg.extraPlugins == [] then pg
- else pkgs.buildEnv {
- name = "postgresql-and-plugins-${(builtins.parseDrvName pg.name).version}";
- paths = [ pg pg.lib ] ++ cfg.extraPlugins;
- # We include /bin to ensure the $out/bin directory is created which is
- # needed because we'll be removing files from that directory in postBuild
- # below.
- pathsToLink = [ "/" "/bin" ];
- buildInputs = [ pkgs.makeWrapper ];
- postBuild = ''
- rm $out/bin/{pg_config,postgres,pg_ctl}
- cp --target-directory=$out/bin ${pg}/bin/{postgres,pg_config,pg_ctl}
- wrapProgram $out/bin/postgres --set NIX_PGLIBDIR $out/lib
- '';
- };
+ postgresql =
+ if cfg.extraPlugins == []
+ then cfg.package
+ else cfg.package.withPackages (_: cfg.extraPlugins);
+
+ toStr = value:
+ if true == value then "yes"
+ else if false == value then "no"
+ else if isString value then "'${lib.replaceStrings ["'"] ["''"] value}'"
+ else toString value;
+
+ # The main PostgreSQL configuration file.
+ configFile = pkgs.writeTextDir "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));
+
+ configFileCheck = pkgs.runCommand "postgresql-configfile-check" {} ''
+ ${cfg.package}/bin/postgres -D${configFile} -C config_file >/dev/null
+ touch $out
+ '';
- postgresql = postgresqlAndPlugins cfg.package;
+ groupAccessAvailable = versionAtLeast postgresql.version "11.0";
- configFile = pkgs.writeText "postgresql.conf"
- ''
- log_destination = 'stderr'
- port = ${toString cfg.port}
- ${cfg.extraConfig}
- '';
in
{
+ imports = [
+ (mkRemovedOptionModule [ "services" "postgresql" "extraConfig" ] "Use services.postgresql.settings instead.")
+ ];
+
+ ###### interface
+
options = {
+
services.postgresql = {
- enable = mkOption {
- type = types.bool;
- default = false;
- description = ''Whether to run PostgreSQL.'';
- };
+
+ enable = mkEnableOption "PostgreSQL Server";
package = mkOption {
type = types.package;
- default = pkgs.postgresql96;
- defaultText = "pkgs.postgresql96";
- description = ''PostgreSQL package to use.'';
+ example = literalExpression "pkgs.postgresql_11";
+ description = ''
+ PostgreSQL package to use.
+ '';
};
port = mkOption {
type = types.int;
default = 5432;
- description = ''The port on which PostgreSQL listens.'';
+ description = ''
+ The port on which PostgreSQL listens.
+ '';
+ };
+
+ checkConfig = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Check the syntax of the configuration file at compile time";
};
dataDir = mkOption {
type = types.path;
- default = "/var/lib/postgresql";
- example = "/var/lib/postgresql/9.6";
- description = ''Data directory for PostgreSQL.'';
+ defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
+ example = "/var/lib/postgresql/11";
+ description = ''
+ The data directory for PostgreSQL. If left as the default value
+ this directory will automatically be created before the PostgreSQL server starts, otherwise
+ the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+ and permissions.
+ '';
};
- characterSet = mkOption {
- type = types.str;
- default = "UTF8";
- example = "SJIS";
- description = ''Character set specified during initialization'';
+ authentication = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Defines how users authenticate themselves to the server. See the
+
+ PostgreSQL documentation for pg_hba.conf
+ for details on the expected format of this option. By default,
+ peer based authentication will be used for users connecting
+ via the Unix socket, and md5 password authentication will be
+ used for users connecting via TCP. Any added rules will be
+ inserted above the default rules. If you'd like to replace the
+ default rules entirely, you can use lib.mkForce in your
+ module.
+ '';
+ };
+
+ identMap = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Defines the mapping from system users to database users.
+
+ The general form is:
+
+ map-name system-username database-username
+ '';
+ };
+
+ initdbArgs = mkOption {
+ type = with types; listOf str;
+ default = [];
+ example = [ "--data-checksums" "--allow-group-access" ];
+ description = ''
+ Additional arguments passed to initdb during data dir
+ initialisation.
+ '';
+ };
+
+ initialScript = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ A file containing SQL statements to execute on first startup.
+ '';
+ };
+
+ ensureDatabases = mkOption {
+ type = types.listOf types.str;
+ 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.
+ '';
+ example = [
+ "gitea"
+ "nextcloud"
+ ];
+ };
+
+ 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 ''${attrValue} ON ''${attrName}.
+ '';
+ example = literalExpression ''
+ {
+ "DATABASE \"nextcloud\"" = "ALL PRIVILEGES";
+ "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
+ }
+ '';
+ };
+ };
+ });
+ default = [];
+ description = ''
+ Ensures that the specified users exist and have at least the ensured permissions.
+ The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the
+ same name only, and that without the need for a password.
+ This option will never delete existing users or remove permissions, especially not when the value of this
+ option is changed. This means that users created and permissions assigned once through this option or
+ otherwise have to be removed manually.
+ '';
+ example = literalExpression ''
+ [
+ {
+ name = "nextcloud";
+ ensurePermissions = {
+ "DATABASE nextcloud" = "ALL PRIVILEGES";
+ };
+ }
+ {
+ name = "superuser";
+ ensurePermissions = {
+ "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
+ };
+ }
+ ]
+ '';
};
enableTCPIP = mkOption {
@@ -78,41 +207,153 @@ in
'';
};
+ logLinePrefix = mkOption {
+ type = types.str;
+ default = "[%p] ";
+ example = "%m [%p] ";
+ description = ''
+ A printf-style string that is output at the beginning of each log line.
+ Upstream default is '%m [%p] ', i.e. it includes the timestamp. We do
+ not include the timestamp, because journal has it anyway.
+ '';
+ };
+
extraPlugins = mkOption {
type = types.listOf types.path;
default = [];
- example = literalExpression "[ (pkgs.postgis.override { postgresql = pkgs.postgresql94; }) ]";
+ example = literalExpression "with pkgs.postgresql_11.pkgs; [ postgis pg_repack ]";
description = ''
- When this list contains elements a new store path is created.
- PostgreSQL and the elements are symlinked into it. Then pg_config,
- postgres and pg_ctl are copied to make them use the new
- $out/lib directory as pkglibdir. This makes it possible to use postgis
- without patching the .sql files which reference $libdir/postgis-1.5.
+ List of PostgreSQL plugins. PostgreSQL version for each plugin should
+ match version for services.postgresql.package value.
'';
};
- extraConfig = mkOption {
- type = types.lines;
- default = "";
- description = "Additional text to be appended to postgresql.conf.";
+ settings = mkOption {
+ type = with types; attrsOf (oneOf [ bool float int str ]);
+ default = {};
+ description = ''
+ PostgreSQL configuration. Refer to
+
+ for an overview of postgresql.conf.
+
+
+ String values will automatically be enclosed in single quotes. Single quotes will be
+ escaped with two single quotes as described by the upstream documentation linked above.
+
+ '';
+ example = literalExpression ''
+ {
+ log_connections = true;
+ log_statement = "all";
+ logging_collector = true
+ log_disconnections = true
+ log_destination = lib.mkForce "syslog";
+ }
+ '';
};
+
+ recoveryConfig = mkOption {
+ type = types.nullOr types.lines;
+ default = null;
+ description = ''
+ Contents of the recovery.conf file.
+ '';
+ };
+
+ superUser = mkOption {
+ type = types.str;
+ default = "postgres";
+ internal = true;
+ readOnly = true;
+ description = ''
+ PostgreSQL superuser account to use for various operations. Internal since changing
+ this value would lead to breakage while setting up databases.
+ '';
+ };
};
+
};
+ ###### implementation
+
config = mkIf cfg.enable {
+ # FIXME: implement. I didn't implement these because they require some
+ # sort of postStart facility, which launchd does not provide.
+ #
+ # one could perhaps trigger another agent by the existing agent, but
+ # I couldn't find how to do that.
+ warnings = if cfg.initialScript != null
+ || cfg.ensureDatabases != []
+ || cfg.ensureUsers != []
+ then [''
+ Currently nix-darwin does not support postgresql initialScript,
+ ensureDatabases, or ensureUsers
+ '']
+ else [];
+
+ services.postgresql.settings =
+ {
+ hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
+ ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
+ log_destination = "stderr";
+ log_line_prefix = cfg.logLinePrefix;
+ listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
+ port = cfg.port;
+ };
+
+ services.postgresql.package = let
+ mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
+ in
+ # Note: when changing the default, make it conditional on
+ # ‘system.stateVersion’ to maintain compatibility with existing
+ # systems!
+ mkDefault (if config.system.stateVersion >= 4 then pkgs.postgresql_14
+ else mkThrow "9_6");
+
+ services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
+
+ services.postgresql.authentication = mkAfter
+ ''
+ # Generated file; do not edit!
+ local all all peer
+ host all all 127.0.0.1/32 md5
+ host all all ::1/128 md5
+ '';
+
environment.systemPackages = [ postgresql ];
+ environment.pathsToLink = [
+ "/share/postgresql"
+ ];
+
+ # FIXME: implement system.extraDependencies to do this less sketchily
+ # system.extraDependencies = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
+
launchd.user.agents.postgresql =
{ path = [ postgresql ];
script = ''
- # Initialise the database.
+ # FIXME: ${if cfg.checkConfig then configFileCheck else ""}
+
if ! test -e ${cfg.dataDir}/PG_VERSION; then
- initdb -U postgres -D ${cfg.dataDir} -E ${cfg.characterSet}
+ # Cleanup the data directory.
+ ${pkgs.coreutils}/bin/rm -f ${cfg.dataDir}/*.conf
+
+ # Initialise the database.
+ ${postgresql}/bin/initdb -U ${cfg.superUser} ${concatStringsSep " " cfg.initdbArgs}
+
+ # See postStart!
+ # FIXME: implement postStart
+ # touch "${cfg.dataDir}/.first_startup"
fi
- ${pkgs.coreutils}/bin/ln -sfn ${configFile} ${cfg.dataDir}/postgresql.conf
- exec ${postgresql}/bin/postgres -D ${cfg.dataDir} ${optionalString cfg.enableTCPIP "-i"}
+ ${pkgs.coreutils}/bin/ln -sfn ${configFile}/postgresql.conf ${cfg.dataDir}/postgresql.conf
+ ${optionalString (cfg.recoveryConfig != null) ''
+ ${pkgs.coreutils}/bin/ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
+ "${cfg.dataDir}/recovery.conf"
+ ''}
+
+ exec ${postgresql}/bin/postgres -D ${cfg.dataDir}
'';
serviceConfig.KeepAlive = true;