at master 8.6 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.patroni; 9 defaultUser = "patroni"; 10 defaultGroup = "patroni"; 11 format = pkgs.formats.yaml { }; 12 13 configFileName = "patroni-${cfg.scope}-${cfg.name}.yaml"; 14 configFile = format.generate configFileName cfg.settings; 15in 16{ 17 imports = [ 18 (lib.mkRemovedOptionModule [ "services" "patroni" "raft" ] '' 19 Raft has been deprecated by upstream. 20 '') 21 (lib.mkRemovedOptionModule [ "services" "patroni" "raftPort" ] '' 22 Raft has been deprecated by upstream. 23 '') 24 ]; 25 26 options.services.patroni = { 27 28 enable = lib.mkEnableOption "Patroni"; 29 30 postgresqlPackage = lib.mkOption { 31 type = lib.types.package; 32 example = lib.literalExpression "pkgs.postgresql_14"; 33 description = '' 34 PostgreSQL package to use. 35 Plugins can be enabled like this `pkgs.postgresql_14.withPackages (p: [ p.pg_safeupdate p.postgis ])`. 36 ''; 37 }; 38 39 postgresqlDataDir = lib.mkOption { 40 type = lib.types.path; 41 defaultText = lib.literalExpression ''"/var/lib/postgresql/''${config.services.patroni.postgresqlPackage.psqlSchema}"''; 42 example = "/var/lib/postgresql/14"; 43 default = "/var/lib/postgresql/${cfg.postgresqlPackage.psqlSchema}"; 44 description = '' 45 The data directory for PostgreSQL. If left as the default value 46 this directory will automatically be created before the PostgreSQL server starts, otherwise 47 the sysadmin is responsible for ensuring the directory exists with appropriate ownership 48 and permissions. 49 ''; 50 }; 51 52 postgresqlPort = lib.mkOption { 53 type = lib.types.port; 54 default = 5432; 55 description = '' 56 The port on which PostgreSQL listens. 57 ''; 58 }; 59 60 user = lib.mkOption { 61 type = lib.types.str; 62 default = defaultUser; 63 example = "postgres"; 64 description = '' 65 The user for the service. If left as the default value this user will automatically be created, 66 otherwise the sysadmin is responsible for ensuring the user exists. 67 ''; 68 }; 69 70 group = lib.mkOption { 71 type = lib.types.str; 72 default = defaultGroup; 73 example = "postgres"; 74 description = '' 75 The group for the service. If left as the default value this group will automatically be created, 76 otherwise the sysadmin is responsible for ensuring the group exists. 77 ''; 78 }; 79 80 dataDir = lib.mkOption { 81 type = lib.types.path; 82 default = "/var/lib/patroni"; 83 description = '' 84 Folder where Patroni data will be written, this is where the pgpass password file will be written. 85 ''; 86 }; 87 88 scope = lib.mkOption { 89 type = lib.types.str; 90 example = "cluster1"; 91 description = '' 92 Cluster name. 93 ''; 94 }; 95 96 name = lib.mkOption { 97 type = lib.types.str; 98 example = "node1"; 99 description = '' 100 The name of the host. Must be unique for the cluster. 101 ''; 102 }; 103 104 namespace = lib.mkOption { 105 type = lib.types.str; 106 default = "/service"; 107 description = '' 108 Path within the configuration store where Patroni will keep information about the cluster. 109 ''; 110 }; 111 112 nodeIp = lib.mkOption { 113 type = lib.types.str; 114 example = "192.168.1.1"; 115 description = '' 116 IP address of this node. 117 ''; 118 }; 119 120 otherNodesIps = lib.mkOption { 121 type = lib.types.listOf lib.types.str; 122 example = [ 123 "192.168.1.2" 124 "192.168.1.3" 125 ]; 126 description = '' 127 IP addresses of the other nodes. 128 ''; 129 }; 130 131 restApiPort = lib.mkOption { 132 type = lib.types.port; 133 default = 8008; 134 description = '' 135 The port on Patroni's REST api listens. 136 ''; 137 }; 138 139 softwareWatchdog = lib.mkOption { 140 type = lib.types.bool; 141 default = false; 142 description = '' 143 This will configure Patroni to use the software watchdog built into the Linux kernel 144 as described in the [documentation](https://patroni.readthedocs.io/en/latest/watchdog.html#setting-up-software-watchdog-on-linux). 145 ''; 146 }; 147 148 settings = lib.mkOption { 149 type = format.type; 150 default = { }; 151 example = { 152 bootstrap = { 153 initdb = [ 154 "encoding=UTF-8" 155 "data-checksums" 156 ]; 157 }; 158 postgresql = { 159 parameters = { 160 unix_socket_directories = "/tmp"; 161 }; 162 }; 163 }; 164 description = '' 165 The primary patroni configuration. See the [documentation](https://patroni.readthedocs.io/en/latest/yaml_configuration.html) 166 for possible values. 167 Secrets should be passed in by using the `environmentFiles` option. 168 ''; 169 }; 170 171 environmentFiles = lib.mkOption { 172 type = 173 with lib.types; 174 attrsOf ( 175 nullOr (oneOf [ 176 str 177 path 178 package 179 ]) 180 ); 181 default = { }; 182 example = { 183 PATRONI_REPLICATION_PASSWORD = "/secret/file"; 184 PATRONI_SUPERUSER_PASSWORD = "/secret/file"; 185 }; 186 description = "Environment variables made available to Patroni as files content, useful for providing secrets from files."; 187 }; 188 }; 189 190 config = lib.mkIf cfg.enable { 191 assertions = [ 192 { 193 assertion = 194 !( 195 cfg.enable 196 && config.services.postgresql.enable 197 && cfg.postgresqlDataDir == config.services.postgresql.dataDir 198 ); 199 message = '' 200 Both services.patroni and services.postgresql are enabled and 201 services.patroni.postgresqlDataDir == services.postgresql.dataDir 202 Disable one or the other, or configure them to use different directories. 203 ''; 204 } 205 ]; 206 207 services.patroni.settings = { 208 scope = cfg.scope; 209 name = cfg.name; 210 namespace = cfg.namespace; 211 212 restapi = { 213 listen = "${cfg.nodeIp}:${toString cfg.restApiPort}"; 214 connect_address = "${cfg.nodeIp}:${toString cfg.restApiPort}"; 215 }; 216 217 postgresql = { 218 listen = "${cfg.nodeIp}:${toString cfg.postgresqlPort}"; 219 connect_address = "${cfg.nodeIp}:${toString cfg.postgresqlPort}"; 220 data_dir = cfg.postgresqlDataDir; 221 bin_dir = "${cfg.postgresqlPackage}/bin"; 222 pgpass = "${cfg.dataDir}/pgpass"; 223 }; 224 225 watchdog = lib.mkIf cfg.softwareWatchdog { 226 mode = "required"; 227 device = "/dev/watchdog"; 228 safety_margin = 5; 229 }; 230 }; 231 232 users = { 233 users = lib.mkIf (cfg.user == defaultUser) { 234 patroni = { 235 group = cfg.group; 236 isSystemUser = true; 237 }; 238 }; 239 groups = lib.mkIf (cfg.group == defaultGroup) { 240 patroni = { }; 241 }; 242 }; 243 244 systemd.services = { 245 patroni = { 246 description = "Runners to orchestrate a high-availability PostgreSQL"; 247 248 wantedBy = [ "multi-user.target" ]; 249 after = [ "network.target" ]; 250 251 script = '' 252 ${lib.concatStringsSep "\n" ( 253 lib.attrValues ( 254 lib.mapAttrs (name: path: ''export ${name}="$(< ${lib.escapeShellArg path})"'') cfg.environmentFiles 255 ) 256 )} 257 exec ${pkgs.patroni}/bin/patroni ${configFile} 258 ''; 259 260 serviceConfig = lib.mkMerge [ 261 { 262 User = cfg.user; 263 Group = cfg.group; 264 Type = "simple"; 265 Restart = "on-failure"; 266 TimeoutSec = 30; 267 ExecReload = "${pkgs.coreutils}/bin/kill -s HUP $MAINPID"; 268 KillMode = "process"; 269 } 270 (lib.mkIf 271 ( 272 cfg.postgresqlDataDir == "/var/lib/postgresql/${cfg.postgresqlPackage.psqlSchema}" 273 && cfg.dataDir == "/var/lib/patroni" 274 ) 275 { 276 StateDirectory = "patroni postgresql postgresql/${cfg.postgresqlPackage.psqlSchema}"; 277 StateDirectoryMode = "0750"; 278 } 279 ) 280 ]; 281 }; 282 }; 283 284 boot.kernelModules = lib.mkIf cfg.softwareWatchdog [ "softdog" ]; 285 286 services.udev.extraRules = lib.mkIf cfg.softwareWatchdog '' 287 KERNEL=="watchdog", OWNER="${cfg.user}", GROUP="${cfg.group}", MODE="0600" 288 ''; 289 290 environment.systemPackages = [ 291 pkgs.patroni 292 cfg.postgresqlPackage 293 ]; 294 295 environment.etc."${configFileName}".source = configFile; 296 297 environment.sessionVariables = { 298 PATRONICTL_CONFIG_FILE = "/etc/${configFileName}"; 299 }; 300 }; 301 302 meta.maintainers = [ lib.maintainers.phfroidmont ]; 303}