at 23.11-pre 5.9 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 pkg = pkgs.pgadmin4; 7 cfg = config.services.pgadmin; 8 9 _base = with types; [ int bool str ]; 10 base = with types; oneOf ([ (listOf (oneOf _base)) (attrsOf (oneOf _base)) ] ++ _base); 11 12 formatAttrset = attr: 13 "{${concatStringsSep "\n" (mapAttrsToList (key: value: "${builtins.toJSON key}: ${formatPyValue value},") attr)}}"; 14 15 formatPyValue = value: 16 if builtins.isString value then builtins.toJSON value 17 else if value ? _expr then value._expr 18 else if builtins.isInt value then toString value 19 else if builtins.isBool value then (if value then "True" else "False") 20 else if builtins.isAttrs value then (formatAttrset value) 21 else if builtins.isList value then "[${concatStringsSep "\n" (map (v: "${formatPyValue v},") value)}]" 22 else throw "Unrecognized type"; 23 24 formatPy = attrs: 25 concatStringsSep "\n" (mapAttrsToList (key: value: "${key} = ${formatPyValue value}") attrs); 26 27 pyType = with types; attrsOf (oneOf [ (attrsOf base) (listOf base) base ]); 28in 29{ 30 options.services.pgadmin = { 31 enable = mkEnableOption (lib.mdDoc "PostgreSQL Admin 4"); 32 33 port = mkOption { 34 description = lib.mdDoc "Port for pgadmin4 to run on"; 35 type = types.port; 36 default = 5050; 37 }; 38 39 initialEmail = mkOption { 40 description = lib.mdDoc "Initial email for the pgAdmin account"; 41 type = types.str; 42 }; 43 44 initialPasswordFile = mkOption { 45 description = lib.mdDoc '' 46 Initial password file for the pgAdmin account. 47 NOTE: Should be string not a store path, to prevent the password from being world readable 48 ''; 49 type = types.path; 50 }; 51 52 emailServer = { 53 enable = mkOption { 54 description = lib.mdDoc '' 55 Enable SMTP email server. This is necessary, if you want to use password recovery or change your own password 56 ''; 57 type = types.bool; 58 default = false; 59 }; 60 address = mkOption { 61 description = lib.mdDoc "SMTP server for email delivery"; 62 type = types.str; 63 default = "localhost"; 64 }; 65 port = mkOption { 66 description = lib.mdDoc "SMTP server port for email delivery"; 67 type = types.port; 68 default = 25; 69 }; 70 useSSL = mkOption { 71 description = lib.mdDoc "SMTP server should use SSL"; 72 type = types.bool; 73 default = false; 74 }; 75 useTLS = mkOption { 76 description = lib.mdDoc "SMTP server should use TLS"; 77 type = types.bool; 78 default = false; 79 }; 80 username = mkOption { 81 description = lib.mdDoc "SMTP server username for email delivery"; 82 type = types.nullOr types.str; 83 default = null; 84 }; 85 sender = mkOption { 86 description = lib.mdDoc '' 87 SMTP server sender email for email delivery. Some servers require this to be a valid email address from that server 88 ''; 89 type = types.str; 90 example = "noreply@example.com"; 91 }; 92 passwordFile = mkOption { 93 description = lib.mdDoc '' 94 Password for SMTP email account. 95 NOTE: Should be string not a store path, to prevent the password from being world readable 96 ''; 97 type = types.path; 98 }; 99 }; 100 101 openFirewall = mkEnableOption (lib.mdDoc "firewall passthrough for pgadmin4"); 102 103 settings = mkOption { 104 description = lib.mdDoc '' 105 Settings for pgadmin4. 106 [Documentation](https://www.pgadmin.org/docs/pgadmin4/development/config_py.html) 107 ''; 108 type = pyType; 109 default = { }; 110 }; 111 }; 112 113 config = mkIf (cfg.enable) { 114 networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ]; 115 116 services.pgadmin.settings = { 117 DEFAULT_SERVER_PORT = cfg.port; 118 SERVER_MODE = true; 119 } // (optionalAttrs cfg.openFirewall { 120 DEFAULT_SERVER = mkDefault "::"; 121 }) // (optionalAttrs cfg.emailServer.enable { 122 MAIL_SERVER = cfg.emailServer.address; 123 MAIL_PORT = cfg.emailServer.port; 124 MAIL_USE_SSL = cfg.emailServer.useSSL; 125 MAIL_USE_TLS = cfg.emailServer.useTLS; 126 MAIL_USERNAME = cfg.emailServer.username; 127 SECURITY_EMAIL_SENDER = cfg.emailServer.sender; 128 }); 129 130 systemd.services.pgadmin = { 131 wantedBy = [ "multi-user.target" ]; 132 after = [ "network.target" ]; 133 requires = [ "network.target" ]; 134 # we're adding this optionally so just in case there's any race it'll be caught 135 # in case postgres doesn't start, pgadmin will just start normally 136 wants = [ "postgresql.service" ]; 137 138 path = [ config.services.postgresql.package pkgs.coreutils pkgs.bash ]; 139 140 preStart = '' 141 # NOTE: this is idempotent (aka running it twice has no effect) 142 ( 143 # Email address: 144 echo ${escapeShellArg cfg.initialEmail} 145 146 # file might not contain newline. echo hack fixes that. 147 PW=$(cat ${escapeShellArg cfg.initialPasswordFile}) 148 149 # Password: 150 echo "$PW" 151 # Retype password: 152 echo "$PW" 153 ) | ${pkg}/bin/pgadmin4-setup 154 ''; 155 156 restartTriggers = [ 157 "/etc/pgadmin/config_system.py" 158 ]; 159 160 serviceConfig = { 161 User = "pgadmin"; 162 DynamicUser = true; 163 LogsDirectory = "pgadmin"; 164 StateDirectory = "pgadmin"; 165 ExecStart = "${pkg}/bin/pgadmin4"; 166 }; 167 }; 168 169 users.users.pgadmin = { 170 isSystemUser = true; 171 group = "pgadmin"; 172 }; 173 174 users.groups.pgadmin = { }; 175 176 environment.etc."pgadmin/config_system.py" = { 177 text = lib.optionalString cfg.emailServer.enable '' 178 with open("${cfg.emailServer.passwordFile}") as f: 179 pw = f.read() 180 MAIL_PASSWORD = pw 181 '' + formatPy cfg.settings; 182 mode = "0600"; 183 user = "pgadmin"; 184 group = "pgadmin"; 185 }; 186 }; 187}