1 { config, lib, pkgs, ... }:
4 cfg = config.services.patroni;
5 defaultUser = "patroni";
6 defaultGroup = "patroni";
7 format = pkgs.formats.yaml { };
9 #boto doesn't support python 3.10 yet
10 patroni = pkgs.patroni.override { pythonPackages = pkgs.python39Packages; };
12 configFileName = "patroni-${cfg.scope}-${cfg.name}.yaml";
13 configFile = format.generate configFileName cfg.settings;
16 options.services.patroni = {
18 enable = mkEnableOption (lib.mdDoc "Patroni");
20 postgresqlPackage = mkOption {
22 example = literalExpression "pkgs.postgresql_14";
23 description = mdDoc ''
24 PostgreSQL package to use.
25 Plugins can be enabled like this `pkgs.postgresql_14.withPackages (p: [ p.pg_safeupdate p.postgis ])`.
29 postgresqlDataDir = mkOption {
31 defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.patroni.postgresqlPackage.psqlSchema}"'';
32 example = "/var/lib/postgresql/14";
33 default = "/var/lib/postgresql/${cfg.postgresqlPackage.psqlSchema}";
34 description = mdDoc ''
35 The data directory for PostgreSQL. If left as the default value
36 this directory will automatically be created before the PostgreSQL server starts, otherwise
37 the sysadmin is responsible for ensuring the directory exists with appropriate ownership
42 postgresqlPort = mkOption {
45 description = mdDoc ''
46 The port on which PostgreSQL listens.
52 default = defaultUser;
54 description = mdDoc ''
55 The user for the service. If left as the default value this user will automatically be created,
56 otherwise the sysadmin is responsible for ensuring the user exists.
62 default = defaultGroup;
64 description = mdDoc ''
65 The group for the service. If left as the default value this group will automatically be created,
66 otherwise the sysadmin is responsible for ensuring the group exists.
72 default = "/var/lib/patroni";
73 description = mdDoc ''
74 Folder where Patroni data will be written, used by Raft as well if enabled.
81 description = mdDoc ''
89 description = mdDoc ''
90 The name of the host. Must be unique for the cluster.
94 namespace = mkOption {
97 description = mdDoc ''
98 Path within the configuration store where Patroni will keep information about the cluster.
104 example = "192.168.1.1";
105 description = mdDoc ''
106 IP address of this node.
110 otherNodesIps = mkOption {
111 type = types.listOf types.string;
112 example = [ "192.168.1.2" "192.168.1.3" ];
113 description = mdDoc ''
114 IP addresses of the other nodes.
118 restApiPort = mkOption {
121 description = mdDoc ''
122 The port on Patroni's REST api listens.
129 description = mdDoc ''
130 This will configure Patroni to use its own RAFT implementation instead of using a dedicated DCS.
134 raftPort = mkOption {
137 description = mdDoc ''
138 The port on which RAFT listens.
142 softwareWatchdog = mkOption {
145 description = mdDoc ''
146 This will configure Patroni to use the software watchdog built into the Linux kernel
147 as described in the [documentation](https://patroni.readthedocs.io/en/latest/watchdog.html#setting-up-software-watchdog-on-linux).
151 settings = mkOption {
154 description = mdDoc ''
155 The primary patroni configuration. See the [documentation](https://patroni.readthedocs.io/en/latest/SETTINGS.html)
157 Secrets should be passed in by using the `environmentFiles` option.
161 environmentFiles = mkOption {
162 type = with types; attrsOf (nullOr (oneOf [ str path package ]));
165 PATRONI_REPLICATION_PASSWORD = "/secret/file";
166 PATRONI_SUPERUSER_PASSWORD = "/secret/file";
168 description = mdDoc "Environment variables made available to Patroni as files content, useful for providing secrets from files.";
172 config = mkIf cfg.enable {
174 services.patroni.settings = {
177 namespace = cfg.namespace;
180 listen = "${cfg.nodeIp}:${toString cfg.restApiPort}";
181 connect_address = "${cfg.nodeIp}:${toString cfg.restApiPort}";
184 raft = mkIf cfg.raft {
185 data_dir = "${cfg.dataDir}/raft";
186 self_addr = "${cfg.nodeIp}:5010";
187 partner_addrs = map (ip: ip + ":5010") cfg.otherNodesIps;
191 listen = "${cfg.nodeIp}:${toString cfg.postgresqlPort}";
192 connect_address = "${cfg.nodeIp}:${toString cfg.postgresqlPort}";
193 data_dir = cfg.postgresqlDataDir;
194 bin_dir = "${cfg.postgresqlPackage}/bin";
195 pgpass = "${cfg.dataDir}/pgpass";
198 watchdog = mkIf cfg.softwareWatchdog {
200 device = "/dev/watchdog";
207 users = mkIf (cfg.user == defaultUser) {
213 groups = mkIf (cfg.group == defaultGroup) {
220 description = "Runners to orchestrate a high-availability PostgreSQL";
222 wantedBy = [ "multi-user.target" ];
223 after = [ "network.target" ];
226 ${concatStringsSep "\n" (attrValues (mapAttrs (name: path: ''export ${name}="$(< ${escapeShellArg path})"'') cfg.environmentFiles))}
227 exec ${patroni}/bin/patroni ${configFile}
230 serviceConfig = mkMerge [
235 Restart = "on-failure";
237 ExecReload = "${pkgs.coreutils}/bin/kill -s HUP $MAINPID";
238 KillMode = "process";
240 (mkIf (cfg.postgresqlDataDir == "/var/lib/postgresql/${cfg.postgresqlPackage.psqlSchema}" && cfg.dataDir == "/var/lib/patroni") {
241 StateDirectory = "patroni patroni/raft postgresql postgresql/${cfg.postgresqlPackage.psqlSchema}";
242 StateDirectoryMode = "0750";
248 boot.kernelModules = mkIf cfg.softwareWatchdog [ "softdog" ];
250 services.udev.extraRules = mkIf cfg.softwareWatchdog ''
251 KERNEL=="watchdog", OWNER="${cfg.user}", GROUP="${cfg.group}", MODE="0600"
254 environment.systemPackages = [
256 cfg.postgresqlPackage
257 (mkIf cfg.raft pkgs.python310Packages.pysyncobj)
260 environment.etc."${configFileName}".source = configFile;
262 environment.sessionVariables = {
263 PATRONICTL_CONFIG_FILE = "/etc/${configFileName}";
267 meta.maintainers = [ maintainers.phfroidmont ];