1058 lines
44 KiB
Nix
1058 lines
44 KiB
Nix
{
|
|
description = "Postgresql";
|
|
|
|
inputs = {
|
|
nixpkgs = {
|
|
url = "nixpkgs/nixos-unstable";
|
|
};
|
|
flake-utils = {
|
|
url = "github:numtide/flake-utils";
|
|
};
|
|
};
|
|
outputs = {
|
|
self,
|
|
nixpkgs,
|
|
flake-utils,
|
|
...
|
|
} @ inputs:
|
|
(
|
|
flake-utils.lib.eachDefaultSystem
|
|
(
|
|
system: let
|
|
pkgs = import nixpkgs {
|
|
inherit system;
|
|
};
|
|
in {
|
|
packages = {
|
|
scram-sha-256 = let
|
|
pname = "scram-sha-256";
|
|
version = "1.0.3";
|
|
in
|
|
pkgs.buildGoModule {
|
|
inherit pname version;
|
|
src = pkgs.fetchFromGitHub {
|
|
owner = "supercaracal";
|
|
repo = pname;
|
|
rev = "v${version}";
|
|
hash = "sha256-55dEihU9FnKiGt9jRJsY1+NRUgOkwoLF8J60RRYG7yM=";
|
|
};
|
|
vendorHash = "sha256-HjyD30RFf5vnZ8CNU1s3sTTyCof1yD8cdVWC7cLwjic=";
|
|
meta = with pkgs.lib; {
|
|
homepage = "https://github.com/supercaracal/scram-sha-256";
|
|
description = "A backup program that is fast, efficient and secure";
|
|
platforms = platforms.all;
|
|
license = licenses.mit;
|
|
};
|
|
};
|
|
pgvecto-rs = let
|
|
pname = "pgvecto.rs";
|
|
version = "0.1.11";
|
|
hashes = {
|
|
"15" = "sha256-IVx/LgRnGyvBRYvrrJatd7yboWEoSYSJogLaH5N/wPA=";
|
|
};
|
|
major = pkgs.lib.versions.major pkgs.postgresql_15.version;
|
|
in
|
|
pkgs.stdenv.mkDerivation {
|
|
inherit pname version;
|
|
|
|
buildInputs = [pkgs.dpkg];
|
|
|
|
src = pkgs.fetchurl {
|
|
url = "https://github.com/tensorchord/pgvecto.rs/releases/download/v${version}/vectors-pg${major}-v${version}-x86_64-unknown-linux-gnu.deb";
|
|
hash = hashes."${major}";
|
|
};
|
|
|
|
dontUnpack = true;
|
|
dontBuild = true;
|
|
dontStrip = true;
|
|
|
|
installPhase = ''
|
|
mkdir -p $out
|
|
dpkg -x $src $out
|
|
install -D -t $out/lib $out/usr/lib/postgresql/${major}/lib/*.so
|
|
install -D -t $out/share/postgresql/extension $out/usr/share/postgresql/${major}/extension/*.sql
|
|
install -D -t $out/share/postgresql/extension $out/usr/share/postgresql/${major}/extension/*.control
|
|
rm -rf $out/usr
|
|
'';
|
|
|
|
meta = {
|
|
description = "Scalable Vector database plugin for Postgres, written in Rust, specifically designed for LLM";
|
|
homepage = "https://github.com/tensorchord/pgvecto.rs";
|
|
license = pkgs.lib.licenses.asl20;
|
|
platforms = pkgs.postgresql.meta.platforms;
|
|
};
|
|
};
|
|
};
|
|
}
|
|
)
|
|
)
|
|
// {
|
|
nixosModules = {
|
|
postgresql = {
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}: let
|
|
cfg = config.jcollie.postgresql;
|
|
in {
|
|
options = {
|
|
jcollie.postgresql = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.options.mkEnableOption "PostgreSQL";
|
|
port = lib.options.mkOption {
|
|
type = lib.types.port;
|
|
default = 5432;
|
|
};
|
|
maxConnections = lib.options.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 100;
|
|
};
|
|
superuser = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
username = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
default = "postgres";
|
|
};
|
|
password = lib.options.mkOption {
|
|
type = lib.types.addCheck lib.types.str (x: lib.hasPrefix "SCRAM-SHA-256$" x);
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
plugins = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
pgvecto-rs = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.options.mkOption {
|
|
description = "Enable pgvecto.rs plugin";
|
|
type = lib.types.bool;
|
|
default = true;
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
postgis = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.options.mkOption {
|
|
description = "Enable postgis plugin";
|
|
type = lib.types.bool;
|
|
default = true;
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
replication = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.options.mkEnableOption "Enable PostgreSQL replication";
|
|
role = lib.options.mkOption {
|
|
type = lib.types.enum [
|
|
"primary"
|
|
"replica"
|
|
];
|
|
default = "primary";
|
|
};
|
|
username = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
default = "replication";
|
|
};
|
|
passwordFile = lib.options.mkOption {
|
|
type = lib.types.path;
|
|
};
|
|
primary = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
hostname = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
port = lib.options.mkOption {
|
|
type = lib.types.port;
|
|
default = 5432;
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
backup = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.options.mkEnableOption "Backup the database with PGBackRest";
|
|
processMax = lib.options.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 4;
|
|
};
|
|
log = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = let
|
|
logLevelType = lib.types.enum [
|
|
"off"
|
|
"error"
|
|
"warn"
|
|
"info"
|
|
"detail"
|
|
"debug"
|
|
"trace"
|
|
];
|
|
in {
|
|
console = lib.options.mkOption {
|
|
type = logLevelType;
|
|
default = "detail";
|
|
};
|
|
file = lib.options.mkOption {
|
|
type = logLevelType;
|
|
default = "off";
|
|
};
|
|
stderr = lib.options.mkOption {
|
|
type = logLevelType;
|
|
default = "off";
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
cipher = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
passwordFile = lib.options.mkOption {
|
|
type = lib.types.path;
|
|
description = "Path to file containing encryption password.";
|
|
};
|
|
type = lib.options.mkOption {
|
|
type = lib.types.enum [
|
|
"aes-256-cbc"
|
|
];
|
|
default = "aes-256-cbc";
|
|
description = "Type of encryption to use.";
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
storage = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
type = lib.options.mkOption {
|
|
type = lib.types.enum [
|
|
"azure"
|
|
"b2"
|
|
];
|
|
description = "The type of storage used for backups.";
|
|
};
|
|
azure = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
accountName = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
accountKeyFile = lib.options.mkOption {
|
|
type = lib.types.path;
|
|
};
|
|
container = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
path = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
description = "Options for connecting to Azure Blob storage";
|
|
};
|
|
b2 = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
accountId = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
accountKeyFile = lib.options.mkOption {
|
|
type = lib.types.path;
|
|
};
|
|
endpoint = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
region = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
bucket = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
path = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
description = "Options for connection to BackBlaze B2";
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
healthcheck = lib.options.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.options.mkEnableOption "use healthchecks";
|
|
fullBackupPingURL = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
differentialBackupPingURL = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
incrementalBackupPingURL = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
|
|
users = lib.options.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
username = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
password = lib.options.mkOption {
|
|
type = lib.types.addCheck lib.types.str (x: lib.hasPrefix "SCRAM-SHA-256$" x);
|
|
description = "Encrypted password for the database account.";
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = [];
|
|
};
|
|
|
|
databases = lib.options.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
name = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
owner = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
};
|
|
extensions = lib.options.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [];
|
|
};
|
|
template = lib.options.mkOption {
|
|
type = lib.types.enum ["template0" "template1"];
|
|
default = "template1";
|
|
};
|
|
encoding = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
default = "UTF8";
|
|
};
|
|
lc_collate = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
default = "en_US.utf8";
|
|
};
|
|
lc_ctype = lib.options.mkOption {
|
|
type = lib.types.str;
|
|
default = "en_US.utf8";
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = [];
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
};
|
|
|
|
config = let
|
|
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;
|
|
|
|
escapeShell = value: lib.replaceStrings ["$"] ["\\$"] value;
|
|
|
|
postgresql = pkgs.postgresql_15.withPackages (
|
|
plugins:
|
|
(
|
|
lib.optionals cfg.plugins.postgis.enable [
|
|
plugins.postgis
|
|
]
|
|
)
|
|
++ (
|
|
lib.optionals cfg.plugins.pgvecto-rs.enable [
|
|
self.packages.${pkgs.system}.pgvecto-rs
|
|
]
|
|
)
|
|
);
|
|
|
|
# rcloneConfig = pkgs.writeTextFile {
|
|
# name = "rclone.conf";
|
|
# text = {
|
|
# "b2" = ''
|
|
# [b2]
|
|
# type = b2
|
|
# account = ${cfg.backup.storage.b2.accountId}
|
|
# key = ${cfg.backup.storage.b2.accountKey}
|
|
# '';
|
|
# "azure" = ''
|
|
# [azure]
|
|
# type = azureblob
|
|
# account = ${cfg.backup.storage.azure.accountName}
|
|
# key = ${cfg.backup.storage.azure.accountKey}
|
|
# '';
|
|
# }.${cfg.backup.storage.type};
|
|
# };
|
|
|
|
rcloneEnvironment =
|
|
{
|
|
RCLONE_CONFIG = "/dev/null";
|
|
}
|
|
// (
|
|
{
|
|
"azure" = {
|
|
RCLONE_CONFIG_AZURE_TYPE = "azureblob";
|
|
RCLONE_CONFIG_AZURE_ACCOUNT = cfg.backup.storage.azure.accountName;
|
|
};
|
|
"b2" = {
|
|
RCLONE_CONFIG_B2_TYPE = "b2";
|
|
RCLONE_CONFIG_B2_ACCOUNT = cfg.backup.storage.b2.accountId;
|
|
};
|
|
}
|
|
.${cfg.backup.storage.type}
|
|
);
|
|
rcloneEnvironmentFiles =
|
|
{
|
|
"azure" = {
|
|
RCLONE_CONFIG_AZURE_KEY = cfg.backup.storage.azure.accountKeyFile;
|
|
};
|
|
"b2" = {
|
|
RCLONE_CONFIG_B2_KEY = cfg.backup.storage.b2.accountKeyFile;
|
|
};
|
|
}
|
|
.${cfg.backup.storage.type};
|
|
|
|
rclone = lib.mkIf (cfg.backup.enable) (
|
|
let
|
|
environment = lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList
|
|
(
|
|
n: v: ''export ${n}="${builtins.toString v}"''
|
|
)
|
|
rcloneEnvironment
|
|
);
|
|
environmentFiles = lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList
|
|
(
|
|
n: v: ''export ${n}=''$(<${v})''
|
|
)
|
|
rcloneEnvironmentFiles
|
|
);
|
|
in
|
|
pkgs.writeShellScriptBin "rclone" ''
|
|
${environment}
|
|
${environmentFiles}
|
|
exec ${pkgs.rclone}/bin/rclone "''$@"
|
|
''
|
|
);
|
|
|
|
rootDir = "/var/lib/postgresql";
|
|
dataDir = "${rootDir}/15";
|
|
|
|
pgbackrestEnvironment =
|
|
{
|
|
PGBACKREST_LOG_LEVEL_CONSOLE = cfg.backup.log.console;
|
|
PGBACKREST_LOG_LEVEL_FILE = cfg.backup.log.file;
|
|
PGBACKREST_LOG_LEVEL_STDERR = cfg.backup.log.stderr;
|
|
PGBACKREST_PG1_PATH = dataDir;
|
|
PGBACKREST_PROCESS_MAX = "${builtins.toString cfg.backup.processMax}";
|
|
PGBACKREST_REPO1_CIPHER_TYPE = cfg.backup.cipher.type;
|
|
PGBACKREST_REPO1_RETENTION_FULL = "14";
|
|
PGBACKREST_REPO1_RETENTION_FULL_TYPE = "time";
|
|
PGBACKREST_STANZA = "${config.networking.hostName}.${config.networking.domain}";
|
|
}
|
|
// (
|
|
{
|
|
"azure" = {
|
|
PGBACKREST_REPO1_AZURE_ACCOUNT = cfg.backup.storage.azure.accountName;
|
|
PGBACKREST_REPO1_AZURE_CONTAINER = cfg.backup.storage.azure.container;
|
|
PGBACKREST_REPO1_PATH = cfg.backup.storage.azure.path;
|
|
PGBACKREST_REPO1_TYPE = "azure";
|
|
};
|
|
"b2" = {
|
|
PGBACKREST_REPO1_PATH = cfg.backup.storage.b2.path;
|
|
PGBACKREST_REPO1_S3_BUCKET = cfg.backup.storage.b2.bucket;
|
|
PGBACKREST_REPO1_S3_ENDPOINT = cfg.backup.storage.b2.endpoint;
|
|
PGBACKREST_REPO1_S3_KEY = cfg.backup.storage.b2.accountId;
|
|
PGBACKREST_REPO1_S3_REGION = cfg.backup.storage.b2.region;
|
|
PGBACKREST_REPO1_TYPE = "s3";
|
|
};
|
|
}
|
|
.${cfg.backup.storage.type}
|
|
);
|
|
|
|
pgbackrestEnvironmentFiles =
|
|
{
|
|
PGBACKREST_REPO1_CIPHER_PASS = cfg.backup.cipher.passwordFile;
|
|
}
|
|
// {
|
|
"azure" = {
|
|
PGBACKREST_REPO1_AZURE_KEY = cfg.backup.storage.azure.accountKeyFile;
|
|
};
|
|
"b2" = {
|
|
PGBACKREST_REPO1_S3_KEY_SECRET = cfg.backup.storage.b2.accountKeyFile;
|
|
};
|
|
}
|
|
.${cfg.backup.storage.type};
|
|
|
|
pgbackrest =
|
|
if (cfg.backup.enable)
|
|
then
|
|
(
|
|
let
|
|
environment = lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList
|
|
(
|
|
n: v: ''export ${n}="${builtins.toString v}"''
|
|
)
|
|
pgbackrestEnvironment
|
|
);
|
|
environmentFiles = lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList
|
|
(
|
|
n: v: ''export ${n}=''$(<${v})''
|
|
)
|
|
pgbackrestEnvironmentFiles
|
|
);
|
|
in
|
|
pkgs.writeShellScriptBin "pgbackrest" ''
|
|
${environment}
|
|
${environmentFiles}
|
|
exec ${pkgs.pgbackrest}/bin/pgbackrest "''$@"
|
|
''
|
|
)
|
|
else {};
|
|
# pgbackrestnu = lib.mkIf (cfg.backup.enable) (
|
|
# let
|
|
# environment = writeText "pgbackrest-environment.json" (builtins.toJSON pgbackrestEnvironment);
|
|
# in
|
|
# pkgs.writeScriptBin "pgbackrest" ''
|
|
# #!${pkgs.nushell}/bin/nu
|
|
|
|
# def main [...args: string] {
|
|
# load-env (open ${data})
|
|
|
|
# exec ${pkgs.pgbackrest}/bin/pgbackrest $args
|
|
# }
|
|
# ''
|
|
# );
|
|
curl = "${pkgs.curl}/bin/curl --silent --show-error --max-time 10 --retry 5";
|
|
in
|
|
lib.mkIf cfg.enable {
|
|
assertions = [
|
|
# {
|
|
# assertion = !(cfg.backup.storage.b2.accountKey != null && cfg.backup.storage.b2.accountKeyFile != null);
|
|
# message = "cannot set both accountKey and accountKeyFile on B2 storage";
|
|
# }
|
|
];
|
|
|
|
environment.systemPackages =
|
|
[
|
|
postgresql
|
|
self.packages.${pkgs.system}.scram-sha-256
|
|
]
|
|
++ (
|
|
if cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")
|
|
then [
|
|
pgbackrest
|
|
rclone
|
|
]
|
|
else []
|
|
);
|
|
|
|
# networking.firewall.allowedTCPPorts = [ cfg.port ];
|
|
|
|
security.acme.certs."${config.networking.hostName}.${config.networking.domain}" = {
|
|
reloadServices = [
|
|
"postgresql"
|
|
];
|
|
};
|
|
|
|
users.groups.postgres.gid = config.ids.gids.postgres;
|
|
users.users.postgres = {
|
|
name = "postgres";
|
|
uid = config.ids.uids.postgres;
|
|
group = "postgres";
|
|
description = "PostgreSQL Server";
|
|
home = dataDir;
|
|
useDefaultShell = true;
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${rootDir} 0750 postgres postgres -"
|
|
"d ${dataDir} 0700 postgres postgres -"
|
|
];
|
|
|
|
systemd.services.postgresql = let
|
|
hbaFile = pkgs.writeTextDir "pg_hba.conf" ''
|
|
local all all ident map=default
|
|
hostnossl all all all reject
|
|
hostssl all all all scram-sha-256
|
|
local replication all ident map=default
|
|
hostnossl replication all all reject
|
|
hostssl replication all all scram-sha-256
|
|
'';
|
|
identFile = pkgs.writeTextDir "pg_ident.conf" (
|
|
''
|
|
default root postgres
|
|
default postgres postgres
|
|
''
|
|
+ (
|
|
lib.strings.concatStringsSep "\n"
|
|
(
|
|
map
|
|
(user: "default root ${user.username}")
|
|
cfg.users
|
|
)
|
|
)
|
|
+ "\n"
|
|
);
|
|
archiveCommand = "${pgbackrest}/bin/pgbackrest archive-push %p";
|
|
settings =
|
|
{
|
|
bgwriter_flush_after = "512kB";
|
|
checkpoint_flush_after = "256kB";
|
|
data_directory = dataDir;
|
|
datestyle = "iso, mdy";
|
|
default_text_search_config = "pg_catalog.english";
|
|
dynamic_shared_memory_type = "posix";
|
|
hba_file = "${dataDir}/pg_hba.conf";
|
|
hot_standby = "on";
|
|
ident_file = "${dataDir}/pg_ident.conf";
|
|
lc_messages = "en_US.utf8";
|
|
lc_monetary = "en_US.utf8";
|
|
lc_numeric = "en_US.utf8";
|
|
lc_time = "en_US.utf8";
|
|
listen_addresses = "*";
|
|
log_destination = "stderr";
|
|
log_connections = "on";
|
|
log_line_prefix = "%m [%l][%p] {%h} :%d:%u:%c: ";
|
|
# log_statement = "all";
|
|
log_timezone = "America/Chicago";
|
|
logging_collector = "off";
|
|
max_connections = cfg.maxConnections;
|
|
max_wal_senders = 3;
|
|
max_wal_size = "1GB";
|
|
min_wal_size = "80MB";
|
|
password_encryption = "scram-sha-256";
|
|
port = cfg.port;
|
|
shared_buffers = "128MB";
|
|
shared_preload_libraries = lib.strings.concatStringsSep "," (lib.optionals cfg.plugins.pgvecto-rs.enable ["vectors.so"]);
|
|
ssl = "on";
|
|
ssl_cert_file = "/run/credentials/postgresql.service/fullchain.pem";
|
|
ssl_key_file = "/run/credentials/postgresql.service/key.pem";
|
|
timezone = "America/Chicago";
|
|
unix_socket_directories = "/run/postgresql";
|
|
wal_level = "replica";
|
|
wal_log_hints = "on";
|
|
}
|
|
// (
|
|
if (cfg.backup.enable)
|
|
then {
|
|
archive_command = archiveCommand;
|
|
archive_mode = "on";
|
|
}
|
|
else {}
|
|
);
|
|
configFile = pkgs.writeTextDir "postgresql.conf" (
|
|
lib.concatStrings (
|
|
lib.mapAttrsToList
|
|
(
|
|
n: v: "${n} = ${toStr v}\n"
|
|
)
|
|
settings
|
|
)
|
|
);
|
|
backupSetup =
|
|
if (cfg.backup.enable)
|
|
then ''
|
|
init=''$(${pgbackrest}/bin/pgbackrest info --output=json | ${pkgs.jq}/bin/jq '.[0].status.code == 0')
|
|
if [ "$init" != "true" ]
|
|
then
|
|
${postgresql}/bin/pg_ctl -o "-c ssl=off -c listen_addresses=''' -p 55432" -w start
|
|
${pgbackrest}/bin/pgbackrest --pg1-port=55432 stanza-create
|
|
${postgresql}/bin/pg_ctl -m fast -w stop
|
|
fi
|
|
''
|
|
else "";
|
|
pgpass =
|
|
if (cfg.replication.enable && cfg.replication.role == "replica")
|
|
then
|
|
lib.concatStringsSep ":" [
|
|
cfg.replication.primary.hostname
|
|
(builtins.toString cfg.replication.primary.port)
|
|
"replication"
|
|
cfg.replication.username
|
|
''''$(<${cfg.replication.passwordFile})''
|
|
]
|
|
else "";
|
|
in {
|
|
description = "PostgreSQL Server";
|
|
wantedBy = ["multi-user.target"];
|
|
after = ["network.target"];
|
|
environment =
|
|
{
|
|
PGDATA = dataDir;
|
|
}
|
|
// (
|
|
if (cfg.replication.enable && cfg.replication.role == "replica")
|
|
then {
|
|
PGPASSFILE = "${rootDir}/.pgpass";
|
|
}
|
|
else {}
|
|
);
|
|
path =
|
|
[
|
|
postgresql
|
|
]
|
|
++ (
|
|
if cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")
|
|
then [
|
|
pgbackrest
|
|
]
|
|
else []
|
|
);
|
|
|
|
preStart =
|
|
if (!cfg.replication.enable || cfg.replication.role == "primary")
|
|
then ''
|
|
if [ ! -s "${dataDir}/PG_VERSION" ]
|
|
then
|
|
${postgresql}/bin/initdb \
|
|
--username=${cfg.superuser.username} \
|
|
--pwfile=<(echo -n "${escapeShell cfg.superuser.password}") \
|
|
--data-checksums \
|
|
--locale=en_US.utf8 \
|
|
--auth-host=scram-sha-256 \
|
|
--auth-local=trust
|
|
fi
|
|
|
|
ln -sfn "${configFile}/postgresql.conf" "${dataDir}/postgresql.conf"
|
|
ln -sfn "${hbaFile}/pg_hba.conf" "${dataDir}/pg_hba.conf"
|
|
ln -sfn "${identFile}/pg_ident.conf" "${dataDir}/pg_ident.conf"
|
|
|
|
${backupSetup}
|
|
''
|
|
else ''
|
|
(umask 077; echo "${pgpass}" > ${rootDir}/.pgpass)
|
|
chmod 0600 ${rootDir}/.pgpass
|
|
|
|
if [ ! -s "${dataDir}/PG_VERSION" ]
|
|
then
|
|
${postgresql}/bin/pg_basebackup \
|
|
--verbose \
|
|
--progress \
|
|
--pgdata=${dataDir} \
|
|
--write-recovery-conf \
|
|
--slot=${config.networking.hostName} \
|
|
--create-slot \
|
|
--wal-method=stream \
|
|
--dbname=postgresql://${cfg.replication.username}@${cfg.replication.primary.hostname}:${builtins.toString cfg.replication.primary.port}/postgresql?sslmode=require \
|
|
--no-password
|
|
fi
|
|
|
|
ln -sfn "${configFile}/postgresql.conf" "${dataDir}/postgresql.conf"
|
|
ln -sfn "${hbaFile}/pg_hba.conf" "${dataDir}/pg_hba.conf"
|
|
ln -sfn "${identFile}/pg_ident.conf" "${dataDir}/pg_ident.conf"
|
|
'';
|
|
|
|
unitConfig = {
|
|
RequiresMountsFor = "${dataDir}";
|
|
};
|
|
serviceConfig = {
|
|
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
|
ExecStart = "${postgresql}/bin/postgres";
|
|
Group = "postgres";
|
|
KillMode = "mixed";
|
|
KillSignal = "SIGINT";
|
|
RuntimeDirectory = "postgresql";
|
|
StateDirectory = "postgresql";
|
|
StateDirectoryMode = "0750";
|
|
TimeoutSec = 300;
|
|
Type = "notify";
|
|
User = "postgres";
|
|
LoadCredential = [
|
|
"fullchain.pem:${config.security.acme.certs."${config.networking.hostName}.${config.networking.domain}".directory}/fullchain.pem"
|
|
"key.pem:${config.security.acme.certs."${config.networking.hostName}.${config.networking.domain}".directory}/key.pem"
|
|
];
|
|
};
|
|
};
|
|
|
|
systemd.services.postgresql-setup = lib.mkIf (!cfg.replication.enable || cfg.replication.role == "primary") (
|
|
let
|
|
replicationSetup =
|
|
if (cfg.replication.enable && cfg.replication.role == "primary")
|
|
then ''
|
|
if $PSQL --command "SELECT 1 FROM pg_roles WHERE rolname='${cfg.replication.username}';" | grep -q 1
|
|
then
|
|
echo "alter replication user ${cfg.replication.username}"
|
|
echo "ALTER ROLE ${cfg.replication.username} WITH REPLICATION LOGIN PASSWORD :'v1';" | $PSQL \
|
|
--variable=v1="''$(<''${CREDENTIALS_DIRECTORY}/postgresql-replication-password)"
|
|
else
|
|
echo "create replication user ${cfg.replication.username}"
|
|
echo "CREATE ROLE ${cfg.replication.username} WITH REPLICATION LOGIN PASSWORD :'v1';" | $PSQL \
|
|
--variable=v1="''$(<''${CREDENTIALS_DIRECTORY}/postgresql-replication-password)"
|
|
fi
|
|
''
|
|
else "";
|
|
userSetup =
|
|
lib.strings.concatStringsSep "\n"
|
|
(
|
|
map
|
|
(
|
|
user: ''
|
|
if $PSQL --command "SELECT 1 FROM pg_roles WHERE rolname='${user.username}';" | grep -q 1
|
|
then
|
|
echo "alter user ${user.username}"
|
|
echo "ALTER ROLE :username WITH LOGIN PASSWORD :'password';" | $PSQL --variable username="${user.username}" --variable password="${escapeShell user.password}"
|
|
else
|
|
echo "create user ${user.username}"
|
|
echo "CREATE ROLE :username WITH LOGIN PASSWORD :'password';" | $PSQL --variable username="${user.username}" --variable password="${escapeShell user.password}"
|
|
fi
|
|
''
|
|
)
|
|
cfg.users
|
|
);
|
|
nuShellDatabaseSetup = ''
|
|
|
|
'';
|
|
databaseSetup = lib.strings.concatStringsSep "\n" (
|
|
map
|
|
(
|
|
database:
|
|
''
|
|
if ! ( echo "SELECT 1 FROM pg_database WHERE datname=:'name';" | $PSQL --variable name="${database.name}" | grep -q 1 )
|
|
then
|
|
echo "create database ${database.name}"
|
|
echo "CREATE DATABASE :name WITH OWNER = :'owner' TEMPLATE = :'template' ENCODING = :'encoding' LC_COLLATE = :'lc_collate' LC_CTYPE = :'lc_ctype';" | $PSQL --variable name="${database.name}" --variable owner="${database.owner}" --variable encoding="${database.encoding}" --variable lc_collate="${database.lc_collate}" --variable lc_ctype="${database.lc_ctype}" --variable template="${database.template}"
|
|
fi
|
|
|
|
echo "grant public schema priviliges to user ${database.owner}"
|
|
echo "GRANT ALL PRIVILEGES ON SCHEMA public TO :owner;" | $PSQL --dbname "${database.name}" --variable name="${database.name}" --variable owner="${database.owner}"
|
|
echo "grant priviliges on database ${database.name} to user ${database.owner}"
|
|
echo "GRANT ALL PRIVILEGES ON DATABASE :name TO :owner;" | $PSQL --dbname "${database.name}" --variable name="${database.name}" --variable owner="${database.owner}"
|
|
''
|
|
+ (
|
|
lib.strings.concatStringsSep "\n" (
|
|
map
|
|
(
|
|
extension: ''
|
|
if ! ( $PSQL --dbname ${database.name} --command "SELECT 1 FROM pg_extension WHERE extname='${extension}';" | grep -q 1 )
|
|
then
|
|
echo "adding extention ${extension} to ${database.name}"
|
|
$PSQL --dbname ${database.name} --command "CREATE EXTENSION ${extension};"
|
|
fi
|
|
''
|
|
)
|
|
database.extensions
|
|
)
|
|
)
|
|
)
|
|
cfg.databases
|
|
);
|
|
in {
|
|
description = "PostgreSQL User/Database Setup";
|
|
after = ["postgresql.service"];
|
|
# bindsTo = [ "postgresql.service" ];
|
|
script = ''
|
|
while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null
|
|
do
|
|
sleep 0.1
|
|
done
|
|
|
|
PSQL="${postgresql}/bin/psql --tuples-only --no-align"
|
|
|
|
$PSQL --command "ALTER ROLE ${cfg.superuser.username} WITH SUPERUSER LOGIN PASSWORD '${escapeShell cfg.superuser.password}';"
|
|
|
|
${replicationSetup}
|
|
${userSetup}
|
|
${databaseSetup}
|
|
'';
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "postgres";
|
|
Group = "postgres";
|
|
RemainAfterExit = true;
|
|
TimeoutSec = 120;
|
|
LoadCredential = [
|
|
"postgresql-replication-password:${cfg.replication.passwordFile}"
|
|
];
|
|
};
|
|
}
|
|
);
|
|
|
|
systemd.services.postgresql-backup-full = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) (
|
|
let
|
|
hcStart =
|
|
if cfg.backup.healthcheck.enable
|
|
then "${curl} ${cfg.backup.healthcheck.fullBackupPingURL}/start"
|
|
else "";
|
|
hcStop =
|
|
if cfg.backup.healthcheck.enable
|
|
then "${curl} ${cfg.backup.healthcheck.fullBackupPingURL}"
|
|
else "";
|
|
in {
|
|
description = "PostgreSQL Full Backup";
|
|
requires = ["postgresql.service"];
|
|
script = ''
|
|
${hcStart}
|
|
|
|
while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null
|
|
do
|
|
sleep 0.1
|
|
done
|
|
|
|
${pgbackrest}/bin/pgbackrest --type=full --start-fast --stop-auto --delta backup
|
|
|
|
${hcStop}
|
|
'';
|
|
environment = pgbackrestEnvironment;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "postgres";
|
|
Group = "postgres";
|
|
TimeoutSec = "4h";
|
|
};
|
|
}
|
|
);
|
|
|
|
systemd.timers.postgresql-backup-full = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) {
|
|
description = "PostgreSQL Full Backup";
|
|
timerConfig = {
|
|
OnCalendar = "Sun *-*-* 02:00:00";
|
|
RandomizedDelaySec = "5m";
|
|
};
|
|
wantedBy = ["multi-user.target"];
|
|
};
|
|
|
|
systemd.services.postgresql-backup-diff = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) (
|
|
let
|
|
hcStart =
|
|
if cfg.backup.healthcheck.enable
|
|
then "${curl} ${cfg.backup.healthcheck.differentialBackupPingURL}/start"
|
|
else "";
|
|
hcStop =
|
|
if cfg.backup.healthcheck.enable
|
|
then "${curl} ${cfg.backup.healthcheck.differentialBackupPingURL}"
|
|
else "";
|
|
in {
|
|
description = "PostgreSQL Differential Backup";
|
|
requires = ["postgresql.service"];
|
|
script = ''
|
|
${hcStart}
|
|
|
|
while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null
|
|
do
|
|
sleep 0.1
|
|
done
|
|
|
|
${pgbackrest}/bin/pgbackrest --type=diff --start-fast --stop-auto --delta backup
|
|
|
|
${hcStop}
|
|
'';
|
|
environment = pgbackrestEnvironment;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "postgres";
|
|
Group = "postgres";
|
|
TimeoutSec = 3600;
|
|
};
|
|
}
|
|
);
|
|
|
|
systemd.timers.postgresql-backup-diff = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) {
|
|
description = "PostgreSQL Differential Backup";
|
|
timerConfig = {
|
|
OnCalendar = "Mon,Tue,Wed,Thu,Fri,Sat *-*-* 02:00:00";
|
|
RandomizedDelaySec = "5m";
|
|
};
|
|
wantedBy = ["multi-user.target"];
|
|
};
|
|
|
|
systemd.services.postgresql-backup-incr = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) (
|
|
let
|
|
hcStart =
|
|
if cfg.backup.healthcheck.enable
|
|
then "${curl} ${cfg.backup.healthcheck.incrementalBackupPingURL}/start"
|
|
else "";
|
|
hcStop =
|
|
if cfg.backup.healthcheck.enable
|
|
then "${curl} ${cfg.backup.healthcheck.incrementalBackupPingURL}"
|
|
else "";
|
|
in {
|
|
description = "PostgreSQL Incremental Backup";
|
|
requires = ["postgresql.service"];
|
|
script = ''
|
|
${hcStart}
|
|
|
|
while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null
|
|
do
|
|
sleep 0.1
|
|
done
|
|
|
|
${pgbackrest}/bin/pgbackrest --type=incr --start-fast --stop-auto --delta backup
|
|
|
|
${hcStop}
|
|
'';
|
|
environment = pgbackrestEnvironment;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "postgres";
|
|
Group = "postgres";
|
|
TimeoutSec = 3600;
|
|
};
|
|
}
|
|
);
|
|
|
|
systemd.timers.postgresql-backup-incr = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) {
|
|
description = "PostgreSQL Incremental Backup";
|
|
timerConfig = {
|
|
OnCalendar = "*-*-* 00,06,10,14,18,22:00:00";
|
|
RandomizedDelaySec = "5m";
|
|
};
|
|
wantedBy = ["multi-user.target"];
|
|
};
|
|
|
|
services.prometheus.exporters.postgres = {
|
|
enable = true;
|
|
runAsLocalSuperUser = true;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|