at 23.05-pre 15 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.services.postgresql; 8 9 postgresql = 10 if cfg.extraPlugins == [] 11 then cfg.package 12 else cfg.package.withPackages (_: cfg.extraPlugins); 13 14 toStr = value: 15 if true == value then "yes" 16 else if false == value then "no" 17 else if isString value then "'${lib.replaceStrings ["'"] ["''"] value}'" 18 else toString value; 19 20 # The main PostgreSQL configuration file. 21 configFile = pkgs.writeTextDir "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings)); 22 23 configFileCheck = pkgs.runCommand "postgresql-configfile-check" {} '' 24 ${cfg.package}/bin/postgres -D${configFile} -C config_file >/dev/null 25 touch $out 26 ''; 27 28 groupAccessAvailable = versionAtLeast postgresql.version "11.0"; 29 30in 31 32{ 33 imports = [ 34 (mkRemovedOptionModule [ "services" "postgresql" "extraConfig" ] "Use services.postgresql.settings instead.") 35 ]; 36 37 ###### interface 38 39 options = { 40 41 services.postgresql = { 42 43 enable = mkEnableOption (lib.mdDoc "PostgreSQL Server"); 44 45 package = mkOption { 46 type = types.package; 47 example = literalExpression "pkgs.postgresql_11"; 48 description = lib.mdDoc '' 49 PostgreSQL package to use. 50 ''; 51 }; 52 53 port = mkOption { 54 type = types.int; 55 default = 5432; 56 description = lib.mdDoc '' 57 The port on which PostgreSQL listens. 58 ''; 59 }; 60 61 checkConfig = mkOption { 62 type = types.bool; 63 default = true; 64 description = lib.mdDoc "Check the syntax of the configuration file at compile time"; 65 }; 66 67 dataDir = mkOption { 68 type = types.path; 69 defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"''; 70 example = "/var/lib/postgresql/11"; 71 description = lib.mdDoc '' 72 The data directory for PostgreSQL. If left as the default value 73 this directory will automatically be created before the PostgreSQL server starts, otherwise 74 the sysadmin is responsible for ensuring the directory exists with appropriate ownership 75 and permissions. 76 ''; 77 }; 78 79 authentication = mkOption { 80 type = types.lines; 81 default = ""; 82 description = lib.mdDoc '' 83 Defines how users authenticate themselves to the server. See the 84 [PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) 85 for details on the expected format of this option. By default, 86 peer based authentication will be used for users connecting 87 via the Unix socket, and md5 password authentication will be 88 used for users connecting via TCP. Any added rules will be 89 inserted above the default rules. If you'd like to replace the 90 default rules entirely, you can use `lib.mkForce` in your 91 module. 92 ''; 93 }; 94 95 identMap = mkOption { 96 type = types.lines; 97 default = ""; 98 description = lib.mdDoc '' 99 Defines the mapping from system users to database users. 100 101 The general form is: 102 103 map-name system-username database-username 104 ''; 105 }; 106 107 initdbArgs = mkOption { 108 type = with types; listOf str; 109 default = []; 110 example = [ "--data-checksums" "--allow-group-access" ]; 111 description = lib.mdDoc '' 112 Additional arguments passed to `initdb` during data dir 113 initialisation. 114 ''; 115 }; 116 117 initialScript = mkOption { 118 type = types.nullOr types.path; 119 default = null; 120 description = lib.mdDoc '' 121 A file containing SQL statements to execute on first startup. 122 ''; 123 }; 124 125 ensureDatabases = mkOption { 126 type = types.listOf types.str; 127 default = []; 128 description = lib.mdDoc '' 129 Ensures that the specified databases exist. 130 This option will never delete existing databases, especially not when the value of this 131 option is changed. This means that databases created once through this option or 132 otherwise have to be removed manually. 133 ''; 134 example = [ 135 "gitea" 136 "nextcloud" 137 ]; 138 }; 139 140 ensureUsers = mkOption { 141 type = types.listOf (types.submodule { 142 options = { 143 name = mkOption { 144 type = types.str; 145 description = lib.mdDoc '' 146 Name of the user to ensure. 147 ''; 148 }; 149 ensurePermissions = mkOption { 150 type = types.attrsOf types.str; 151 default = {}; 152 description = lib.mdDoc '' 153 Permissions to ensure for the user, specified as an attribute set. 154 The attribute names specify the database and tables to grant the permissions for. 155 The attribute values specify the permissions to grant. You may specify one or 156 multiple comma-separated SQL privileges here. 157 158 For more information on how to specify the target 159 and on which privileges exist, see the 160 [GRANT syntax](https://www.postgresql.org/docs/current/sql-grant.html). 161 The attributes are used as `GRANT ''${attrValue} ON ''${attrName}`. 162 ''; 163 example = literalExpression '' 164 { 165 "DATABASE \"nextcloud\"" = "ALL PRIVILEGES"; 166 "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES"; 167 } 168 ''; 169 }; 170 }; 171 }); 172 default = []; 173 description = lib.mdDoc '' 174 Ensures that the specified users exist and have at least the ensured permissions. 175 The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the 176 same name only, and that without the need for a password. 177 This option will never delete existing users or remove permissions, especially not when the value of this 178 option is changed. This means that users created and permissions assigned once through this option or 179 otherwise have to be removed manually. 180 ''; 181 example = literalExpression '' 182 [ 183 { 184 name = "nextcloud"; 185 ensurePermissions = { 186 "DATABASE nextcloud" = "ALL PRIVILEGES"; 187 }; 188 } 189 { 190 name = "superuser"; 191 ensurePermissions = { 192 "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES"; 193 }; 194 } 195 ] 196 ''; 197 }; 198 199 enableTCPIP = mkOption { 200 type = types.bool; 201 default = false; 202 description = lib.mdDoc '' 203 Whether PostgreSQL should listen on all network interfaces. 204 If disabled, the database can only be accessed via its Unix 205 domain socket or via TCP connections to localhost. 206 ''; 207 }; 208 209 logLinePrefix = mkOption { 210 type = types.str; 211 default = "[%p] "; 212 example = "%m [%p] "; 213 description = lib.mdDoc '' 214 A printf-style string that is output at the beginning of each log line. 215 Upstream default is `'%m [%p] '`, i.e. it includes the timestamp. We do 216 not include the timestamp, because journal has it anyway. 217 ''; 218 }; 219 220 extraPlugins = mkOption { 221 type = types.listOf types.path; 222 default = []; 223 example = literalExpression "with pkgs.postgresql_11.pkgs; [ postgis pg_repack ]"; 224 description = lib.mdDoc '' 225 List of PostgreSQL plugins. PostgreSQL version for each plugin should 226 match version for `services.postgresql.package` value. 227 ''; 228 }; 229 230 settings = mkOption { 231 type = with types; attrsOf (oneOf [ bool float int str ]); 232 default = {}; 233 description = lib.mdDoc '' 234 PostgreSQL configuration. Refer to 235 <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE> 236 for an overview of `postgresql.conf`. 237 238 ::: {.note} 239 String values will automatically be enclosed in single quotes. Single quotes will be 240 escaped with two single quotes as described by the upstream documentation linked above. 241 ::: 242 ''; 243 example = literalExpression '' 244 { 245 log_connections = true; 246 log_statement = "all"; 247 logging_collector = true 248 log_disconnections = true 249 log_destination = lib.mkForce "syslog"; 250 } 251 ''; 252 }; 253 254 recoveryConfig = mkOption { 255 type = types.nullOr types.lines; 256 default = null; 257 description = lib.mdDoc '' 258 Contents of the {file}`recovery.conf` file. 259 ''; 260 }; 261 262 superUser = mkOption { 263 type = types.str; 264 default = "postgres"; 265 internal = true; 266 readOnly = true; 267 description = lib.mdDoc '' 268 PostgreSQL superuser account to use for various operations. Internal since changing 269 this value would lead to breakage while setting up databases. 270 ''; 271 }; 272 }; 273 274 }; 275 276 277 ###### implementation 278 279 config = mkIf cfg.enable { 280 281 services.postgresql.settings = 282 { 283 hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}"; 284 ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}"; 285 log_destination = "stderr"; 286 log_line_prefix = cfg.logLinePrefix; 287 listen_addresses = if cfg.enableTCPIP then "*" else "localhost"; 288 port = cfg.port; 289 }; 290 291 services.postgresql.package = let 292 mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version."; 293 in 294 # Note: when changing the default, make it conditional on 295 # ‘system.stateVersion’ to maintain compatibility with existing 296 # systems! 297 mkDefault (if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14 298 else if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13 299 else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11 300 else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6" 301 else mkThrow "9_5"); 302 303 services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}"; 304 305 services.postgresql.authentication = mkAfter 306 '' 307 # Generated file; do not edit! 308 local all all peer 309 host all all 127.0.0.1/32 md5 310 host all all ::1/128 md5 311 ''; 312 313 users.users.postgres = 314 { name = "postgres"; 315 uid = config.ids.uids.postgres; 316 group = "postgres"; 317 description = "PostgreSQL server user"; 318 home = "${cfg.dataDir}"; 319 useDefaultShell = true; 320 }; 321 322 users.groups.postgres.gid = config.ids.gids.postgres; 323 324 environment.systemPackages = [ postgresql ]; 325 326 environment.pathsToLink = [ 327 "/share/postgresql" 328 ]; 329 330 system.extraDependencies = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck; 331 332 systemd.services.postgresql = 333 { description = "PostgreSQL Server"; 334 335 wantedBy = [ "multi-user.target" ]; 336 after = [ "network.target" ]; 337 338 environment.PGDATA = cfg.dataDir; 339 340 path = [ postgresql ]; 341 342 preStart = 343 '' 344 if ! test -e ${cfg.dataDir}/PG_VERSION; then 345 # Cleanup the data directory. 346 rm -f ${cfg.dataDir}/*.conf 347 348 # Initialise the database. 349 initdb -U ${cfg.superUser} ${concatStringsSep " " cfg.initdbArgs} 350 351 # See postStart! 352 touch "${cfg.dataDir}/.first_startup" 353 fi 354 355 ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf" 356 ${optionalString (cfg.recoveryConfig != null) '' 357 ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \ 358 "${cfg.dataDir}/recovery.conf" 359 ''} 360 ''; 361 362 # Wait for PostgreSQL to be ready to accept connections. 363 postStart = 364 '' 365 PSQL="psql --port=${toString cfg.port}" 366 367 while ! $PSQL -d postgres -c "" 2> /dev/null; do 368 if ! kill -0 "$MAINPID"; then exit 1; fi 369 sleep 0.1 370 done 371 372 if test -e "${cfg.dataDir}/.first_startup"; then 373 ${optionalString (cfg.initialScript != null) '' 374 $PSQL -f "${cfg.initialScript}" -d postgres 375 ''} 376 rm -f "${cfg.dataDir}/.first_startup" 377 fi 378 '' + optionalString (cfg.ensureDatabases != []) '' 379 ${concatMapStrings (database: '' 380 $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${database}"' 381 '') cfg.ensureDatabases} 382 '' + '' 383 ${concatMapStrings (user: '' 384 $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"' 385 ${concatStringsSep "\n" (mapAttrsToList (database: permission: '' 386 $PSQL -tAc 'GRANT ${permission} ON ${database} TO "${user.name}"' 387 '') user.ensurePermissions)} 388 '') cfg.ensureUsers} 389 ''; 390 391 serviceConfig = mkMerge [ 392 { ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 393 User = "postgres"; 394 Group = "postgres"; 395 RuntimeDirectory = "postgresql"; 396 Type = if versionAtLeast cfg.package.version "9.6" 397 then "notify" 398 else "simple"; 399 400 # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See 401 # http://www.postgresql.org/docs/current/static/server-shutdown.html 402 KillSignal = "SIGINT"; 403 KillMode = "mixed"; 404 405 # Give Postgres a decent amount of time to clean up after 406 # receiving systemd's SIGINT. 407 TimeoutSec = 120; 408 409 ExecStart = "${postgresql}/bin/postgres"; 410 } 411 (mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") { 412 StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}"; 413 StateDirectoryMode = if groupAccessAvailable then "0750" else "0700"; 414 }) 415 ]; 416 417 unitConfig.RequiresMountsFor = "${cfg.dataDir}"; 418 }; 419 420 }; 421 422 meta.doc = ./postgresql.xml; 423 meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ]; 424}