grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / pretalx.nix
blob35af8c26482c7b3fb490ea5eb3183a0000c77dba
1 { config
2 , lib
3 , pkgs
4 , utils
5 , ...
6 }:
8 let
9   cfg = config.services.pretalx;
10   format = pkgs.formats.ini { };
12   configFile = format.generate "pretalx.cfg" cfg.settings;
14   finalPackage = cfg.package.override {
15     inherit (cfg) plugins;
16   };
18   pythonEnv = finalPackage.python.buildEnv.override {
19     extraLibs = with finalPackage.python.pkgs; [
20       (toPythonModule finalPackage)
21       gunicorn
22     ]
23     ++ finalPackage.optional-dependencies.redis
24     ++ lib.optionals cfg.celery.enable [ celery ]
25     ++ lib.optionals (cfg.settings.database.backend == "mysql") finalPackage.optional-dependencies.mysql
26     ++ lib.optionals (cfg.settings.database.backend == "postgresql") finalPackage.optional-dependencies.postgres;
27   };
31   meta = with lib; {
32     maintainers = with maintainers; [ hexa] ++ teams.c3d2.members;
33   };
35   options.services.pretalx = {
36     enable = lib.mkEnableOption "pretalx";
38     package = lib.mkPackageOption pkgs "pretalx" {};
40     group = lib.mkOption {
41       type = lib.types.str;
42       default = "pretalx";
43       description = "Group under which pretalx should run.";
44     };
46     user = lib.mkOption {
47       type = lib.types.str;
48       default = "pretalx";
49       description = "User under which pretalx should run.";
50     };
52     plugins = lib.mkOption {
53       type = with lib.types; listOf package;
54       default = [];
55       example = lib.literalExpression ''
56         with config.services.pretalx.package.plugins; [
57           pages
58           youtube
59         ];
60       '';
61       description = ''
62         Pretalx plugins to install into the Python environment.
63       '';
64     };
66     gunicorn.extraArgs = lib.mkOption {
67       type = with lib.types; listOf str;
68       default = [
69         "--name=pretalx"
70       ];
71       example = [
72         "--name=pretalx"
73         "--workers=4"
74         "--max-requests=1200"
75         "--max-requests-jitter=50"
76         "--log-level=info"
77       ];
78       description = ''
79         Extra arguments to pass to gunicorn.
80         See <https://docs.pretalx.org/administrator/installation.html#step-6-starting-pretalx-as-a-service> for details.
81       '';
82       apply = lib.escapeShellArgs;
83     };
85     celery = {
86       enable = lib.mkOption {
87         type = lib.types.bool;
88         default = true;
89         example = false;
90         description = ''
91           Whether to set up celery as an asynchronous task runner.
92         '';
93       };
95       extraArgs = lib.mkOption {
96         type = with lib.types; listOf str;
97         default = [ ];
98         description = ''
99           Extra arguments to pass to celery.
101           See <https://docs.celeryq.dev/en/stable/reference/cli.html#celery-worker> for more info.
102         '';
103         apply = utils.escapeSystemdExecArgs;
104       };
105     };
107     nginx = {
108       enable = lib.mkOption {
109         type = lib.types.bool;
110         default = true;
111         example = false;
112         description = ''
113           Whether to set up an nginx virtual host.
114         '';
115       };
117       domain = lib.mkOption {
118         type = lib.types.str;
119         example = "talks.example.com";
120         description = ''
121           The domain name under which to set up the virtual host.
122         '';
123       };
124     };
126     database.createLocally = lib.mkOption {
127       type = lib.types.bool;
128       default = true;
129       example = false;
130       description = ''
131         Whether to automatically set up the database on the local DBMS instance.
133         Currently only supported for PostgreSQL. Not required for sqlite.
134       '';
135     };
137     settings = lib.mkOption {
138       type = lib.types.submodule {
139         freeformType = format.type;
140         options = {
141           database = {
142             backend = lib.mkOption {
143               type = lib.types.enum [
144                 "postgresql"
145               ];
146               default = "postgresql";
147               description = ''
148                 Database backend to use.
150                 Currently only PostgreSQL gets tested, and as such we don't support any other DBMS.
151               '';
152               readOnly = true; # only postgres supported right now
153             };
155             host = lib.mkOption {
156               type = with lib.types; nullOr types.path;
157               default = if cfg.settings.database.backend == "postgresql" then "/run/postgresql"
158                 else if cfg.settings.database.backend == "mysql" then "/run/mysqld/mysqld.sock"
159                 else null;
160               defaultText = lib.literalExpression ''
161                 if config.services.pretalx.settings..database.backend == "postgresql" then "/run/postgresql"
162                 else if config.services.pretalx.settings.database.backend == "mysql" then "/run/mysqld/mysqld.sock"
163                 else null
164               '';
165               description = ''
166                 Database host or socket path.
167               '';
168             };
170             name = lib.mkOption {
171               type = lib.types.str;
172               default = "pretalx";
173               description = ''
174                 Database name.
175               '';
176             };
178             user = lib.mkOption {
179               type = lib.types.str;
180               default = "pretalx";
181               description = ''
182                 Database username.
183               '';
184             };
185           };
187           filesystem = {
188             data = lib.mkOption {
189               type = lib.types.path;
190               default = "/var/lib/pretalx";
191               description = ''
192                 Base path for all other storage paths.
193               '';
194             };
195             logs = lib.mkOption {
196               type = lib.types.path;
197               default = "/var/log/pretalx";
198               description = ''
199                 Path to the log directory, that pretalx logs message to.
200               '';
201             };
202             static = lib.mkOption {
203               type = lib.types.path;
204               default = "${cfg.package.static}/";
205               defaultText = lib.literalExpression "\${config.services.pretalx.package}.static}/";
206               readOnly = true;
207               description = ''
208                 Path to the directory that contains static files.
209               '';
210             };
211           };
213           celery = {
214             backend = lib.mkOption {
215               type = with lib.types; nullOr str;
216               default = lib.optionalString cfg.celery.enable "redis+socket://${config.services.redis.servers.pretalx.unixSocket}?virtual_host=1";
217               defaultText = lib.literalExpression ''
218                 optionalString config.services.pretalx.celery.enable "redis+socket://''${config.services.redis.servers.pretalx.unixSocket}?virtual_host=1"
219               '';
220               description = ''
221                 URI to the celery backend used for the asynchronous job queue.
222               '';
223             };
225             broker = lib.mkOption {
226               type = with lib.types; nullOr str;
227               default = lib.optionalString cfg.celery.enable "redis+socket://${config.services.redis.servers.pretalx.unixSocket}?virtual_host=2";
228               defaultText = lib.literalExpression ''
229                 optionalString config.services.pretalx.celery.enable "redis+socket://''${config.services.redis.servers.pretalx.unixSocket}?virtual_host=2"
230               '';
231               description = ''
232                 URI to the celery broker used for the asynchronous job queue.
233               '';
234             };
235           };
237           redis = {
238             location = lib.mkOption {
239               type = with lib.types; nullOr str;
240               default = "unix://${config.services.redis.servers.pretalx.unixSocket}?db=0";
241               defaultText = lib.literalExpression ''
242                 "unix://''${config.services.redis.servers.pretalx.unixSocket}?db=0"
243               '';
244               description = ''
245                 URI to the redis server, used to speed up locking, caching and session storage.
246               '';
247             };
249             session = lib.mkOption {
250               type = lib.types.bool;
251               default = true;
252               example = false;
253               description = ''
254                 Whether to use redis as the session storage.
255               '';
256             };
257           };
259           site = {
260             url = lib.mkOption {
261               type = lib.types.str;
262               default = "https://${cfg.nginx.domain}";
263               defaultText = lib.literalExpression "https://\${config.services.pretalx.nginx.domain}";
264               example = "https://talks.example.com";
265               description = ''
266                 The base URI below which your pretalx instance will be reachable.
267               '';
268             };
269           };
270         };
271       };
272       default = { };
273       description = ''
274         pretalx configuration as a Nix attribute set. All settings can also be passed
275         from the environment.
277         See <https://docs.pretalx.org/administrator/configure.html> for possible options.
278       '';
279     };
280   };
282   config = lib.mkIf cfg.enable {
283     # https://docs.pretalx.org/administrator/installation.html
285     environment.systemPackages = [
286       (pkgs.writeScriptBin "pretalx-manage" ''
287         cd ${cfg.settings.filesystem.data}
288         sudo=exec
289         if [[ "$USER" != ${cfg.user} ]]; then
290           sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env=PRETALX_CONFIG_FILE'
291         fi
292         export PRETALX_CONFIG_FILE=${configFile}
293         $sudo ${lib.getExe' pythonEnv "pretalx-manage"} "$@"
294       '')
295     ];
297     services.logrotate.settings.pretalx = {
298       files = "${cfg.settings.filesystem.logs}/*.log";
299       su = "${cfg.user} ${cfg.group}";
300       frequency = "weekly";
301       rotate = "12";
302       copytruncate = true;
303       compress = true;
304     };
306     services = {
307       nginx = lib.mkIf cfg.nginx.enable {
308         enable = true;
309         recommendedGzipSettings = lib.mkDefault true;
310         recommendedOptimisation = lib.mkDefault true;
311         recommendedProxySettings = lib.mkDefault true;
312         recommendedTlsSettings = lib.mkDefault true;
313         upstreams.pretalx.servers."unix:/run/pretalx/pretalx.sock" = { };
314         virtualHosts.${cfg.nginx.domain} = {
315           # https://docs.pretalx.org/administrator/installation.html#step-7-ssl
316           extraConfig = ''
317             more_set_headers "Referrer-Policy: same-origin";
318             more_set_headers "X-Content-Type-Options: nosniff";
319           '';
320           locations = {
321             "/".proxyPass = "http://pretalx";
322             "/media/" = {
323               alias = "${cfg.settings.filesystem.data}/media/";
324               extraConfig = ''
325                 access_log off;
326                 more_set_headers 'Content-Disposition: attachment; filename="$1"';
327                 expires 7d;
328               '';
329             };
330             "/static/" = {
331               alias = cfg.settings.filesystem.static;
332               extraConfig = ''
333                 access_log off;
334                 more_set_headers Cache-Control "public";
335                 expires 365d;
336               '';
337             };
338           };
339         };
340       };
342       postgresql = lib.mkIf (cfg.database.createLocally && cfg.settings.database.backend == "postgresql") {
343         enable = true;
344         ensureUsers = [ {
345           name = cfg.settings.database.user;
346           ensureDBOwnership = true;
347         } ];
348         ensureDatabases = [ cfg.settings.database.name ];
349       };
351       redis.servers.pretalx.enable = true;
352     };
354     systemd.services = let
355       commonUnitConfig = {
356         environment.PRETALX_CONFIG_FILE = configFile;
357         serviceConfig = {
358           User = "pretalx";
359           Group = "pretalx";
360           StateDirectory = [
361             "pretalx"
362             "pretalx/media"
363           ];
364           StateDirectoryMode = "0750";
365           LogsDirectory = "pretalx";
366           WorkingDirectory = cfg.settings.filesystem.data;
367           SupplementaryGroups = [ "redis-pretalx" ];
368           AmbientCapabilities = "";
369           CapabilityBoundingSet = [ "" ];
370           DevicePolicy = "closed";
371           LockPersonality = true;
372           MemoryDenyWriteExecute = true;
373           NoNewPrivileges = true;
374           PrivateDevices = true;
375           PrivateTmp = true;
376           ProcSubset = "pid";
377           ProtectControlGroups = true;
378           ProtectHome = true;
379           ProtectHostname = true;
380           ProtectKernelLogs = true;
381           ProtectKernelModules = true;
382           ProtectKernelTunables = true;
383           ProtectProc = "invisible";
384           ProtectSystem = "strict";
385           RemoveIPC = true;
386           RestrictAddressFamilies = [
387             "AF_INET"
388             "AF_INET6"
389             "AF_UNIX"
390           ];
391           RestrictNamespaces = true;
392           RestrictRealtime = true;
393           RestrictSUIDSGID = true;
394           SystemCallArchitectures = "native";
395           SystemCallFilter = [
396             "@system-service"
397             "~@privileged"
398             "@chown"
399           ];
400           UMask = "0027";
401         };
402       };
403     in {
404       pretalx-web = lib.recursiveUpdate commonUnitConfig {
405         description = "pretalx web service";
406         after = [
407           "network.target"
408           "redis-pretalx.service"
409         ] ++ lib.optionals (cfg.settings.database.backend == "postgresql") [
410           "postgresql.service"
411         ] ++ lib.optionals (cfg.settings.database.backend == "mysql") [
412           "mysql.service"
413         ];
414         wantedBy = [ "multi-user.target" ];
415         preStart = ''
416           versionFile="${cfg.settings.filesystem.data}/.version"
417           version=$(cat "$versionFile" 2>/dev/null || echo 0)
419           if [[ $version != ${cfg.package.version} ]]; then
420             ${lib.getExe' pythonEnv "pretalx-manage"} migrate
422             echo "${cfg.package.version}" > "$versionFile"
423           fi
424         '';
425         serviceConfig = {
426           ExecStart = "${lib.getExe' pythonEnv "gunicorn"} --bind unix:/run/pretalx/pretalx.sock ${cfg.gunicorn.extraArgs} pretalx.wsgi";
427           RuntimeDirectory = "pretalx";
428         };
429       };
431       pretalx-periodic = lib.recursiveUpdate commonUnitConfig {
432         description = "pretalx periodic task runner";
433         # every 15 minutes
434         startAt = [ "*:3,18,33,48" ];
435         serviceConfig = {
436           Type = "oneshot";
437           ExecStart = "${lib.getExe' pythonEnv "pretalx-manage"} runperiodic";
438         };
439       };
441       pretalx-clear-sessions = lib.recursiveUpdate commonUnitConfig {
442         description = "pretalx session pruning";
443         startAt = [ "monthly" ];
444         serviceConfig = {
445           Type = "oneshot";
446           ExecStart = "${lib.getExe' pythonEnv "pretalx-manage"} clearsessions";
447         };
448       };
450       pretalx-worker = lib.mkIf cfg.celery.enable (lib.recursiveUpdate commonUnitConfig {
451         description = "pretalx asynchronous job runner";
452         after = [
453           "network.target"
454           "redis-pretalx.service"
455         ] ++ lib.optionals (cfg.settings.database.backend == "postgresql") [
456           "postgresql.service"
457         ] ++ lib.optionals (cfg.settings.database.backend == "mysql") [
458           "mysql.service"
459         ];
460         wantedBy = [ "multi-user.target" ];
461         serviceConfig.ExecStart = "${lib.getExe' pythonEnv "celery"} -A pretalx.celery_app worker ${cfg.celery.extraArgs}";
462       });
464       nginx.serviceConfig.SupplementaryGroups = lib.mkIf cfg.nginx.enable [ "pretalx" ];
465     };
467     systemd.sockets.pretalx-web.socketConfig = {
468       ListenStream = "/run/pretalx/pretalx.sock";
469       SocketUser = "nginx";
470     };
472     users = {
473       groups.${cfg.group} = {};
474       users.${cfg.user} = {
475         isSystemUser = true;
476         inherit (cfg) group;
477       };
478     };
479   };