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}