2023-06-20 00:54:46 +03:00
|
|
|
# Based on https://github.com/cachix/devenv/blob/main/src/modules/services/postgres.nix
|
2023-07-17 23:45:33 +03:00
|
|
|
{ name, config, pkgs, lib, ... }:
|
|
|
|
with lib.types; let
|
2023-06-20 00:54:46 +03:00
|
|
|
inherit (lib) types;
|
|
|
|
in
|
|
|
|
{
|
2023-07-17 23:45:33 +03:00
|
|
|
options = {
|
|
|
|
enable = lib.mkEnableOption name;
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
package = lib.mkPackageOption pkgs "postgresql" { };
|
|
|
|
extensions = lib.mkOption {
|
|
|
|
type = with types; nullOr (functionTo (listOf package));
|
|
|
|
default = null;
|
|
|
|
example = lib.literalExpression ''
|
|
|
|
extensions: [
|
|
|
|
extensions.pg_cron
|
|
|
|
extensions.postgis
|
|
|
|
extensions.timescaledb
|
|
|
|
];
|
|
|
|
'';
|
|
|
|
description = ''
|
|
|
|
Additional PostgreSQL extensions to install.
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
The available extensions are:
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
${lib.concatLines (builtins.map (x: "- " + x) (builtins.attrNames pkgs.postgresql.pkgs))}
|
|
|
|
'';
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
dataDir = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
default = "./data/${name}";
|
|
|
|
description = "The DB data directory";
|
|
|
|
};
|
|
|
|
|
|
|
|
hbaConf =
|
|
|
|
let
|
|
|
|
hbaConfSubmodule = lib.types.submodule {
|
|
|
|
options = {
|
|
|
|
type = lib.mkOption { type = lib.types.str; };
|
|
|
|
database = lib.mkOption { type = lib.types.str; };
|
|
|
|
user = lib.mkOption { type = lib.types.str; };
|
|
|
|
address = lib.mkOption { type = lib.types.str; };
|
|
|
|
method = lib.mkOption { type = lib.types.str; };
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
};
|
2023-07-17 23:45:33 +03:00
|
|
|
in
|
|
|
|
lib.mkOption {
|
|
|
|
type = lib.types.listOf hbaConfSubmodule;
|
|
|
|
default = [ ];
|
|
|
|
description = ''
|
|
|
|
A list of objects that represent the entries in the pg_hba.conf file.
|
2023-07-05 08:53:56 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
Each object has sub-options for type, database, user, address, and method.
|
2023-07-05 08:53:56 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
See the official PostgreSQL documentation for more information:
|
|
|
|
https://www.postgresql.org/docs/current/auth-pg-hba-conf.html
|
|
|
|
'';
|
|
|
|
example = [
|
|
|
|
{ type = "local"; database = "all"; user = "postgres"; address = ""; method = "md5"; }
|
|
|
|
{ type = "host"; database = "all"; user = "all"; address = "0.0.0.0/0"; method = "md5"; }
|
|
|
|
];
|
|
|
|
};
|
|
|
|
hbaConfFile =
|
|
|
|
let
|
|
|
|
# Default pg_hba.conf entries
|
|
|
|
defaultHbaConf = [
|
|
|
|
{ type = "local"; database = "all"; user = "all"; address = ""; method = "trust"; }
|
|
|
|
{ type = "host"; database = "all"; user = "all"; address = "127.0.0.1/32"; method = "trust"; }
|
|
|
|
{ type = "host"; database = "all"; user = "all"; address = "::1/128"; method = "trust"; }
|
|
|
|
{ type = "local"; database = "replication"; user = "all"; address = ""; method = "trust"; }
|
|
|
|
{ type = "host"; database = "replication"; user = "all"; address = "127.0.0.1/32"; method = "trust"; }
|
|
|
|
{ type = "host"; database = "replication"; user = "all"; address = "::1/128"; method = "trust"; }
|
|
|
|
];
|
2023-07-05 08:53:56 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
# Merge the default pg_hba.conf entries with the user-defined entries
|
|
|
|
hbaConf = defaultHbaConf ++ config.hbaConf;
|
2023-07-05 08:53:56 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
# Convert the pgHbaConf array to a string
|
|
|
|
hbaConfString = ''
|
|
|
|
# Generated by Nix
|
|
|
|
${"# TYPE\tDATABASE\tUSER\tADDRESS\tMETHOD\n"}
|
|
|
|
${lib.concatMapStrings (cnf: " ${cnf.type}\t${cnf.database}\t${cnf.user}\t${cnf.address}\t${cnf.method}\n") hbaConf}
|
|
|
|
'';
|
|
|
|
in
|
|
|
|
lib.mkOption {
|
|
|
|
type = lib.types.package;
|
|
|
|
internal = true;
|
|
|
|
readOnly = true;
|
|
|
|
description = "The `pg_hba.conf` file.";
|
|
|
|
default = pkgs.writeText "pg_hba.conf" hbaConfString;
|
|
|
|
};
|
2023-07-05 08:53:56 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
listen_addresses = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
|
|
|
description = "Listen address";
|
|
|
|
default = "";
|
|
|
|
example = "127.0.0.1";
|
|
|
|
};
|
2023-07-05 08:53:56 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
port = lib.mkOption {
|
|
|
|
type = lib.types.port;
|
|
|
|
default = 5432;
|
|
|
|
description = ''
|
|
|
|
The TCP port to accept connections.
|
|
|
|
'';
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
createDatabase = lib.mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = ''
|
|
|
|
Create a database named like current user on startup. Only applies when initialDatabases is an empty list.
|
|
|
|
'';
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
initdbArgs = lib.mkOption {
|
|
|
|
type = types.listOf types.lines;
|
|
|
|
default = [ "--locale=C" "--encoding=UTF8" ];
|
|
|
|
example = [ "--data-checksums" "--allow-group-access" ];
|
|
|
|
description = ''
|
|
|
|
Additional arguments passed to `initdb` during data dir
|
|
|
|
initialisation.
|
|
|
|
'';
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-22 01:08:57 +03:00
|
|
|
defaultSettings =
|
|
|
|
lib.mkOption {
|
|
|
|
type = with lib.types; attrsOf (oneOf [ bool float int str ]);
|
|
|
|
internal = true;
|
|
|
|
readOnly = true;
|
|
|
|
description = ''
|
|
|
|
Default configuration for `postgresql.conf`. `settings` can override these values.
|
|
|
|
'';
|
|
|
|
default = {
|
|
|
|
listen_addresses = config.listen_addresses;
|
|
|
|
port = config.port;
|
|
|
|
unix_socket_directories = config.dataDir;
|
|
|
|
hba_file = "${config.hbaConfFile}";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
settings =
|
|
|
|
lib.mkOption {
|
|
|
|
type = with lib.types; attrsOf (oneOf [ bool float int str ]);
|
|
|
|
default = { };
|
|
|
|
description = ''
|
|
|
|
PostgreSQL configuration. Refer to
|
|
|
|
<https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
|
|
|
|
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.
|
|
|
|
'';
|
|
|
|
default = {
|
|
|
|
listen_addresses = config.listen_addresses;
|
|
|
|
port = config.port;
|
|
|
|
unix_socket_directories = lib.mkDefault config.dataDir;
|
|
|
|
hba_file = "${config.hbaConfFile}";
|
2023-06-20 00:54:46 +03:00
|
|
|
};
|
2023-07-17 23:45:33 +03:00
|
|
|
example = lib.literalExpression ''
|
|
|
|
{
|
|
|
|
log_connections = true;
|
|
|
|
log_statement = "all";
|
|
|
|
logging_collector = true
|
|
|
|
log_disconnections = true
|
|
|
|
log_destination = lib.mkForce "syslog";
|
|
|
|
}
|
|
|
|
'';
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
initialDatabases = lib.mkOption {
|
|
|
|
type = types.listOf (types.submodule {
|
|
|
|
options = {
|
|
|
|
name = lib.mkOption {
|
|
|
|
type = types.str;
|
2023-07-05 08:53:56 +03:00
|
|
|
description = ''
|
2023-07-17 23:45:33 +03:00
|
|
|
The name of the database to create.
|
2023-07-05 08:53:56 +03:00
|
|
|
'';
|
2023-07-17 23:45:33 +03:00
|
|
|
};
|
|
|
|
schema = lib.mkOption {
|
|
|
|
type = types.nullOr types.path;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
The initial schema of the database; if null (the default),
|
|
|
|
an empty database is created.
|
2023-07-05 08:53:56 +03:00
|
|
|
'';
|
2023-06-20 00:54:46 +03:00
|
|
|
};
|
|
|
|
};
|
2023-07-17 23:45:33 +03:00
|
|
|
});
|
|
|
|
default = [ ];
|
|
|
|
description = ''
|
|
|
|
List of database names and their initial schemas that should be used to create databases on the first startup
|
|
|
|
of Postgres. The schema attribute is optional: If not specified, an empty database is created.
|
|
|
|
'';
|
|
|
|
example = lib.literalExpression ''
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name = "foodatabase";
|
|
|
|
schema = ./foodatabase.sql;
|
|
|
|
}
|
|
|
|
{ name = "bardatabase"; }
|
|
|
|
]
|
|
|
|
'';
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
initialScript = lib.mkOption {
|
|
|
|
type = types.submodule ({ config, ... }: {
|
|
|
|
options = {
|
|
|
|
before = lib.mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
SQL commands to run before the database initialization.
|
|
|
|
'';
|
|
|
|
example = lib.literalExpression ''
|
|
|
|
CREATE USER postgres SUPERUSER;
|
|
|
|
CREATE USER bar;
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
after = lib.mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
SQL commands to run after the database initialization.
|
|
|
|
'';
|
|
|
|
example = lib.literalExpression ''
|
|
|
|
CREATE TABLE users (
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
name VARCHAR(50) NOT NULL,
|
|
|
|
email VARCHAR(50) NOT NULL UNIQUE
|
|
|
|
);
|
|
|
|
'';
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
};
|
2023-07-17 23:45:33 +03:00
|
|
|
});
|
|
|
|
default = { before = null; after = null; };
|
|
|
|
description = ''
|
|
|
|
Initial SQL commands to run during database initialization. This can be multiple
|
|
|
|
SQL expressions separated by a semi-colon.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
outputs.settings = lib.mkOption {
|
|
|
|
type = types.deferredModule;
|
|
|
|
internal = true;
|
|
|
|
readOnly = true;
|
|
|
|
default =
|
|
|
|
let
|
|
|
|
postgresPkg =
|
|
|
|
if config.extensions != null then
|
|
|
|
if builtins.hasAttr "withPackages" config.package
|
|
|
|
then config.package.withPackages config.extensions
|
2023-06-20 00:54:46 +03:00
|
|
|
else
|
2023-07-17 23:45:33 +03:00
|
|
|
builtins.throw ''
|
|
|
|
Cannot add extensions to the PostgreSQL package.
|
|
|
|
`services.postgres.package` is missing the `withPackages` attribute. Did you already add extensions to the package?
|
|
|
|
''
|
|
|
|
else config.package;
|
|
|
|
in
|
|
|
|
{
|
|
|
|
processes = {
|
|
|
|
# DB initialization
|
|
|
|
"${name}-init".command =
|
2023-07-10 18:40:52 +03:00
|
|
|
let
|
2023-07-17 23:45:33 +03:00
|
|
|
setupInitialDatabases =
|
|
|
|
if config.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}';" | \
|
|
|
|
postgres --single -E 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}";' | postgres --single -E postgres
|
2023-06-20 00:54:46 +03:00
|
|
|
|
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
${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}" | postgres --single -j -E ${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" | postgres --single -j -E ${database.name}
|
|
|
|
done
|
|
|
|
else
|
|
|
|
echo "ERROR: Could not determine how to apply schema with ${database.schema}"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
''}
|
|
|
|
fi
|
|
|
|
'')
|
|
|
|
config.initialDatabases)
|
|
|
|
else
|
|
|
|
lib.optionalString config.createDatabase ''
|
|
|
|
echo "CREATE DATABASE ''${USER:-$(id -nu)};" | postgres --single -E postgres '';
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
runInitialScript =
|
|
|
|
let
|
|
|
|
scriptCmd = sqlScript: ''
|
|
|
|
echo "${sqlScript}" | postgres --single -E postgres
|
|
|
|
'';
|
|
|
|
in
|
|
|
|
{
|
|
|
|
before = with config.initialScript;
|
|
|
|
lib.optionalString (before != null) (scriptCmd before);
|
|
|
|
after = with config.initialScript;
|
|
|
|
lib.optionalString (after != null) (scriptCmd after);
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
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;
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
configFile = pkgs.writeText "postgresql.conf" (lib.concatStringsSep "\n"
|
2023-07-22 01:08:57 +03:00
|
|
|
(lib.mapAttrsToList (n: v: "${n} = ${toStr v}") (config.defaultSettings // config.settings)));
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
setupScript = pkgs.writeShellScriptBin "setup-postgres" ''
|
|
|
|
set -euo pipefail
|
|
|
|
export PATH=${postgresPkg}/bin:${pkgs.coreutils}/bin
|
|
|
|
|
|
|
|
if [[ ! -d "$PGDATA" ]]; then
|
|
|
|
set -x
|
|
|
|
initdb ${lib.concatStringsSep " " config.initdbArgs}
|
|
|
|
set +x
|
|
|
|
|
|
|
|
${runInitialScript.before}
|
|
|
|
${setupInitialDatabases}
|
|
|
|
${runInitialScript.after}
|
|
|
|
else
|
|
|
|
echo "Postgres data directory already exists. Skipping initialization."
|
|
|
|
fi
|
2023-06-20 00:54:46 +03:00
|
|
|
|
2023-07-17 23:45:33 +03:00
|
|
|
# Setup config
|
|
|
|
set -x
|
|
|
|
cp ${configFile} "$PGDATA/postgresql.conf"
|
|
|
|
'';
|
|
|
|
in
|
|
|
|
''
|
|
|
|
export PGDATA="${config.dataDir}"
|
|
|
|
${lib.getExe setupScript}
|
2023-06-21 17:00:45 +03:00
|
|
|
'';
|
2023-07-17 23:45:33 +03:00
|
|
|
|
|
|
|
# DB process
|
|
|
|
${name} =
|
|
|
|
let
|
|
|
|
startScript = pkgs.writeShellApplication {
|
|
|
|
name = "start-postgres";
|
|
|
|
text = ''
|
|
|
|
set -x
|
|
|
|
export PATH="${postgresPkg}"/bin:$PATH
|
|
|
|
PGDATA=$(readlink -f "${config.dataDir}")
|
|
|
|
export PGDATA
|
|
|
|
postgres -k "$PGDATA"
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
in
|
|
|
|
{
|
|
|
|
command = startScript;
|
|
|
|
depends_on."${name}-init".condition = "process_completed_successfully";
|
|
|
|
# SIGINT (= 2) for faster shutdown: https://www.postgresql.org/docs/current/server-shutdown.html
|
|
|
|
shutdown.signal = 2;
|
|
|
|
readiness_probe = {
|
|
|
|
exec.command = "${postgresPkg}/bin/pg_isready -h $(readlink -f ${config.dataDir}) -p ${toString config.port} -d template1";
|
|
|
|
initial_delay_seconds = 2;
|
|
|
|
period_seconds = 10;
|
|
|
|
timeout_seconds = 4;
|
|
|
|
success_threshold = 1;
|
|
|
|
failure_threshold = 5;
|
|
|
|
};
|
|
|
|
# https://github.com/F1bonacc1/process-compose#-auto-restart-if-not-healthy
|
|
|
|
availability.restart = "on_failure";
|
|
|
|
};
|
2023-06-21 17:00:45 +03:00
|
|
|
};
|
2023-07-17 23:45:33 +03:00
|
|
|
};
|
|
|
|
};
|
2023-06-20 00:54:46 +03:00
|
|
|
};
|
|
|
|
}
|