1{ config, pkgs, lib, ... }:
2with lib;
3let
4 inherit (config.services) nginx postfix postgresql redis;
5 inherit (config.users) users groups;
6 cfg = config.services.sourcehut;
7 domain = cfg.settings."sr.ht".global-domain;
8 settingsFormat = pkgs.formats.ini {
9 listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
10 mkKeyValue = k: v:
11 optionalString (v != null)
12 (generators.mkKeyValueDefault {
13 mkValueString = v:
14 if v == true then "yes"
15 else if v == false then "no"
16 else generators.mkValueStringDefault {} v;
17 } "=" k v);
18 };
19 configIniOfService = srv: settingsFormat.generate "sourcehut-${srv}-config.ini"
20 # Each service needs access to only a subset of sections (and secrets).
21 (filterAttrs (k: v: v != null)
22 (mapAttrs (section: v:
23 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" section; in
24 if srvMatch == null # Include sections shared by all services
25 || head srvMatch == srv # Include sections for the service being configured
26 then v
27 # Enable Web links and integrations between services.
28 else if tail srvMatch == [ null ] && cfg.${head srvMatch}.enable
29 then {
30 inherit (v) origin;
31 # mansrht crashes without it
32 oauth-client-id = v.oauth-client-id or null;
33 }
34 # Drop sub-sections of other services
35 else null)
36 (recursiveUpdate cfg.settings {
37 # Those paths are mounted using BindPaths= or BindReadOnlyPaths=
38 # for services needing access to them.
39 "builds.sr.ht::worker".buildlogs = "/var/log/sourcehut/buildsrht-worker";
40 "git.sr.ht".post-update-script = "/usr/bin/gitsrht-update-hook";
41 "git.sr.ht".repos = cfg.settings."git.sr.ht".repos;
42 "hg.sr.ht".changegroup-script = "/usr/bin/hgsrht-hook-changegroup";
43 "hg.sr.ht".repos = cfg.settings."hg.sr.ht".repos;
44 # Making this a per service option despite being in a global section,
45 # so that it uses the redis-server used by the service.
46 "sr.ht".redis-host = cfg.${srv}.redis.host;
47 })));
48 commonServiceSettings = srv: {
49 origin = mkOption {
50 description = lib.mdDoc "URL ${srv}.sr.ht is being served at (protocol://domain)";
51 type = types.str;
52 default = "https://${srv}.${domain}";
53 defaultText = "https://${srv}.example.com";
54 };
55 debug-host = mkOption {
56 description = lib.mdDoc "Address to bind the debug server to.";
57 type = with types; nullOr str;
58 default = null;
59 };
60 debug-port = mkOption {
61 description = lib.mdDoc "Port to bind the debug server to.";
62 type = with types; nullOr str;
63 default = null;
64 };
65 connection-string = mkOption {
66 description = lib.mdDoc "SQLAlchemy connection string for the database.";
67 type = types.str;
68 default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
69 };
70 migrate-on-upgrade = mkEnableOption (lib.mdDoc "automatic migrations on package upgrade") // { default = true; };
71 oauth-client-id = mkOption {
72 description = lib.mdDoc "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
73 type = types.str;
74 };
75 oauth-client-secret = mkOption {
76 description = lib.mdDoc "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
77 type = types.path;
78 apply = s: "<" + toString s;
79 };
80 api-origin = mkOption {
81 description = lib.mdDoc "Origin URL for the API";
82 type = types.str;
83 default = "http://${cfg.listenAddress}:${toString (cfg.${srv}.port + 100)}";
84 defaultText = lib.literalMD ''
85 `"http://''${`[](#opt-services.sourcehut.listenAddress)`}:''${toString (`[](#opt-services.sourcehut.${srv}.port)` + 100)}"`
86 '';
87 };
88 };
89
90 # Specialized python containing all the modules
91 python = pkgs.sourcehut.python.withPackages (ps: with ps; [
92 gunicorn
93 eventlet
94 # For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=1 flower
95 flower
96 # Sourcehut services
97 srht
98 buildsrht
99 gitsrht
100 hgsrht
101 hubsrht
102 listssrht
103 mansrht
104 metasrht
105 # Not a python package
106 #pagessrht
107 pastesrht
108 todosrht
109 ]);
110 mkOptionNullOrStr = description: mkOption {
111 description = lib.mdDoc description;
112 type = with types; nullOr str;
113 default = null;
114 };
115in
116{
117 options.services.sourcehut = {
118 enable = mkEnableOption (lib.mdDoc ''
119 sourcehut - git hosting, continuous integration, mailing list, ticket tracking, wiki
120 and account management services
121 '');
122
123 listenAddress = mkOption {
124 type = types.str;
125 default = "localhost";
126 description = lib.mdDoc "Address to bind to.";
127 };
128
129 python = mkOption {
130 internal = true;
131 type = types.package;
132 default = python;
133 description = lib.mdDoc ''
134 The python package to use. It should contain references to the *srht modules and also
135 gunicorn.
136 '';
137 };
138
139 minio = {
140 enable = mkEnableOption (lib.mdDoc ''local minio integration'');
141 };
142
143 nginx = {
144 enable = mkEnableOption (lib.mdDoc ''local nginx integration'');
145 virtualHost = mkOption {
146 type = types.attrs;
147 default = {};
148 description = lib.mdDoc "Virtual-host configuration merged with all Sourcehut's virtual-hosts.";
149 };
150 };
151
152 postfix = {
153 enable = mkEnableOption (lib.mdDoc ''local postfix integration'');
154 };
155
156 postgresql = {
157 enable = mkEnableOption (lib.mdDoc ''local postgresql integration'');
158 };
159
160 redis = {
161 enable = mkEnableOption (lib.mdDoc ''local redis integration in a dedicated redis-server'');
162 };
163
164 settings = mkOption {
165 type = lib.types.submodule {
166 freeformType = settingsFormat.type;
167 options."sr.ht" = {
168 global-domain = mkOption {
169 description = lib.mdDoc "Global domain name.";
170 type = types.str;
171 example = "example.com";
172 };
173 environment = mkOption {
174 description = lib.mdDoc "Values other than \"production\" adds a banner to each page.";
175 type = types.enum [ "development" "production" ];
176 default = "development";
177 };
178 network-key = mkOption {
179 description = lib.mdDoc ''
180 An absolute file path (which should be outside the Nix-store)
181 to a secret key to encrypt internal messages with. Use `srht-keygen network` to
182 generate this key. It must be consistent between all services and nodes.
183 '';
184 type = types.path;
185 apply = s: "<" + toString s;
186 };
187 owner-email = mkOption {
188 description = lib.mdDoc "Owner's email.";
189 type = types.str;
190 default = "contact@example.com";
191 };
192 owner-name = mkOption {
193 description = lib.mdDoc "Owner's name.";
194 type = types.str;
195 default = "John Doe";
196 };
197 site-blurb = mkOption {
198 description = lib.mdDoc "Blurb for your site.";
199 type = types.str;
200 default = "the hacker's forge";
201 };
202 site-info = mkOption {
203 description = lib.mdDoc "The top-level info page for your site.";
204 type = types.str;
205 default = "https://sourcehut.org";
206 };
207 service-key = mkOption {
208 description = lib.mdDoc ''
209 An absolute file path (which should be outside the Nix-store)
210 to a key used for encrypting session cookies. Use `srht-keygen service` to
211 generate the service key. This must be shared between each node of the same
212 service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
213 different keys. If you configure all of your services with the same
214 config.ini, you may use the same service-key for all of them.
215 '';
216 type = types.path;
217 apply = s: "<" + toString s;
218 };
219 site-name = mkOption {
220 description = lib.mdDoc "The name of your network of sr.ht-based sites.";
221 type = types.str;
222 default = "sourcehut";
223 };
224 source-url = mkOption {
225 description = lib.mdDoc "The source code for your fork of sr.ht.";
226 type = types.str;
227 default = "https://git.sr.ht/~sircmpwn/srht";
228 };
229 };
230 options.mail = {
231 smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
232 smtp-port = mkOption {
233 description = lib.mdDoc "Outgoing SMTP port.";
234 type = with types; nullOr port;
235 default = null;
236 };
237 smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
238 smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
239 smtp-from = mkOption {
240 type = types.str;
241 description = lib.mdDoc "Outgoing SMTP FROM.";
242 };
243 error-to = mkOptionNullOrStr "Address receiving application exceptions";
244 error-from = mkOptionNullOrStr "Address sending application exceptions";
245 pgp-privkey = mkOption {
246 type = types.str;
247 description = lib.mdDoc ''
248 An absolute file path (which should be outside the Nix-store)
249 to an OpenPGP private key.
250
251 Your PGP key information (DO NOT mix up pub and priv here)
252 You must remove the password from your secret key, if present.
253 You can do this with `gpg --edit-key [key-id]`,
254 then use the `passwd` command and do not enter a new password.
255 '';
256 };
257 pgp-pubkey = mkOption {
258 type = with types; either path str;
259 description = lib.mdDoc "OpenPGP public key.";
260 };
261 pgp-key-id = mkOption {
262 type = types.str;
263 description = lib.mdDoc "OpenPGP key identifier.";
264 };
265 };
266 options.objects = {
267 s3-upstream = mkOption {
268 description = lib.mdDoc "Configure the S3-compatible object storage service.";
269 type = with types; nullOr str;
270 default = null;
271 };
272 s3-access-key = mkOption {
273 description = lib.mdDoc "Access key to the S3-compatible object storage service";
274 type = with types; nullOr str;
275 default = null;
276 };
277 s3-secret-key = mkOption {
278 description = lib.mdDoc ''
279 An absolute file path (which should be outside the Nix-store)
280 to the secret key of the S3-compatible object storage service.
281 '';
282 type = with types; nullOr path;
283 default = null;
284 apply = mapNullable (s: "<" + toString s);
285 };
286 };
287 options.webhooks = {
288 private-key = mkOption {
289 description = lib.mdDoc ''
290 An absolute file path (which should be outside the Nix-store)
291 to a base64-encoded Ed25519 key for signing webhook payloads.
292 This should be consistent for all *.sr.ht sites,
293 as this key will be used to verify signatures
294 from other sites in your network.
295 Use the `srht-keygen webhook` command to generate a key.
296 '';
297 type = types.path;
298 apply = s: "<" + toString s;
299 };
300 };
301
302 options."builds.sr.ht" = commonServiceSettings "builds" // {
303 allow-free = mkEnableOption (lib.mdDoc "nonpaying users to submit builds");
304 redis = mkOption {
305 description = lib.mdDoc "The Redis connection used for the Celery worker.";
306 type = types.str;
307 default = "redis+socket:///run/redis-sourcehut-buildsrht/redis.sock?virtual_host=2";
308 };
309 shell = mkOption {
310 description = lib.mdDoc ''
311 Scripts used to launch on SSH connection.
312 `/usr/bin/master-shell` on master,
313 `/usr/bin/runner-shell` on runner.
314 If master and worker are on the same system
315 set to `/usr/bin/runner-shell`.
316 '';
317 type = types.enum ["/usr/bin/master-shell" "/usr/bin/runner-shell"];
318 default = "/usr/bin/master-shell";
319 };
320 };
321 options."builds.sr.ht::worker" = {
322 bind-address = mkOption {
323 description = lib.mdDoc ''
324 HTTP bind address for serving local build information/monitoring.
325 '';
326 type = types.str;
327 default = "localhost:8080";
328 };
329 buildlogs = mkOption {
330 description = lib.mdDoc "Path to write build logs.";
331 type = types.str;
332 default = "/var/log/sourcehut/buildsrht-worker";
333 };
334 name = mkOption {
335 description = lib.mdDoc ''
336 Listening address and listening port
337 of the build runner (with HTTP port if not 80).
338 '';
339 type = types.str;
340 default = "localhost:5020";
341 };
342 timeout = mkOption {
343 description = lib.mdDoc ''
344 Max build duration.
345 See <https://golang.org/pkg/time/#ParseDuration>.
346 '';
347 type = types.str;
348 default = "3m";
349 };
350 };
351
352 options."git.sr.ht" = commonServiceSettings "git" // {
353 outgoing-domain = mkOption {
354 description = lib.mdDoc "Outgoing domain.";
355 type = types.str;
356 default = "https://git.localhost.localdomain";
357 };
358 post-update-script = mkOption {
359 description = lib.mdDoc ''
360 A post-update script which is installed in every git repo.
361 This setting is propagated to newer and existing repositories.
362 '';
363 type = types.path;
364 default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
365 defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
366 };
367 repos = mkOption {
368 description = lib.mdDoc ''
369 Path to git repositories on disk.
370 If changing the default, you must ensure that
371 the gitsrht's user as read and write access to it.
372 '';
373 type = types.str;
374 default = "/var/lib/sourcehut/gitsrht/repos";
375 };
376 webhooks = mkOption {
377 description = lib.mdDoc "The Redis connection used for the webhooks worker.";
378 type = types.str;
379 default = "redis+socket:///run/redis-sourcehut-gitsrht/redis.sock?virtual_host=1";
380 };
381 };
382 options."git.sr.ht::api" = {
383 internal-ipnet = mkOption {
384 description = lib.mdDoc ''
385 Set of IP subnets which are permitted to utilize internal API
386 authentication. This should be limited to the subnets
387 from which your *.sr.ht services are running.
388 See [](#opt-services.sourcehut.listenAddress).
389 '';
390 type = with types; listOf str;
391 default = [ "127.0.0.0/8" "::1/128" ];
392 };
393 };
394
395 options."hg.sr.ht" = commonServiceSettings "hg" // {
396 changegroup-script = mkOption {
397 description = lib.mdDoc ''
398 A changegroup script which is installed in every mercurial repo.
399 This setting is propagated to newer and existing repositories.
400 '';
401 type = types.str;
402 default = "${pkgs.sourcehut.hgsrht}/bin/hgsrht-hook-changegroup";
403 defaultText = "\${pkgs.sourcehut.hgsrht}/bin/hgsrht-hook-changegroup";
404 };
405 repos = mkOption {
406 description = lib.mdDoc ''
407 Path to mercurial repositories on disk.
408 If changing the default, you must ensure that
409 the hgsrht's user as read and write access to it.
410 '';
411 type = types.str;
412 default = "/var/lib/sourcehut/hgsrht/repos";
413 };
414 srhtext = mkOptionNullOrStr ''
415 Path to the srht mercurial extension
416 (defaults to where the hgsrht code is)
417 '';
418 clone_bundle_threshold = mkOption {
419 description = lib.mdDoc ".hg/store size (in MB) past which the nightly job generates clone bundles.";
420 type = types.ints.unsigned;
421 default = 50;
422 };
423 hg_ssh = mkOption {
424 description = lib.mdDoc "Path to hg-ssh (if not in $PATH).";
425 type = types.str;
426 default = "${pkgs.mercurial}/bin/hg-ssh";
427 defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
428 };
429 webhooks = mkOption {
430 description = lib.mdDoc "The Redis connection used for the webhooks worker.";
431 type = types.str;
432 default = "redis+socket:///run/redis-sourcehut-hgsrht/redis.sock?virtual_host=1";
433 };
434 };
435
436 options."hub.sr.ht" = commonServiceSettings "hub" // {
437 };
438
439 options."lists.sr.ht" = commonServiceSettings "lists" // {
440 allow-new-lists = mkEnableOption (lib.mdDoc "creation of new lists");
441 notify-from = mkOption {
442 description = lib.mdDoc "Outgoing email for notifications generated by users.";
443 type = types.str;
444 default = "lists-notify@localhost.localdomain";
445 };
446 posting-domain = mkOption {
447 description = lib.mdDoc "Posting domain.";
448 type = types.str;
449 default = "lists.localhost.localdomain";
450 };
451 redis = mkOption {
452 description = lib.mdDoc "The Redis connection used for the Celery worker.";
453 type = types.str;
454 default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=2";
455 };
456 webhooks = mkOption {
457 description = lib.mdDoc "The Redis connection used for the webhooks worker.";
458 type = types.str;
459 default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=1";
460 };
461 };
462 options."lists.sr.ht::worker" = {
463 reject-mimetypes = mkOption {
464 description = lib.mdDoc ''
465 Comma-delimited list of Content-Types to reject. Messages with Content-Types
466 included in this list are rejected. Multipart messages are always supported,
467 and each part is checked against this list.
468
469 Uses fnmatch for wildcard expansion.
470 '';
471 type = with types; listOf str;
472 default = ["text/html"];
473 };
474 reject-url = mkOption {
475 description = lib.mdDoc "Reject URL.";
476 type = types.str;
477 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
478 };
479 sock = mkOption {
480 description = lib.mdDoc ''
481 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
482 Alternatively, specify IP:PORT and an SMTP server will be run instead.
483 '';
484 type = types.str;
485 default = "/tmp/lists.sr.ht-lmtp.sock";
486 };
487 sock-group = mkOption {
488 description = lib.mdDoc ''
489 The lmtp daemon will make the unix socket group-read/write
490 for users in this group.
491 '';
492 type = types.str;
493 default = "postfix";
494 };
495 };
496
497 options."man.sr.ht" = commonServiceSettings "man" // {
498 };
499
500 options."meta.sr.ht" =
501 removeAttrs (commonServiceSettings "meta")
502 ["oauth-client-id" "oauth-client-secret"] // {
503 webhooks = mkOption {
504 description = lib.mdDoc "The Redis connection used for the webhooks worker.";
505 type = types.str;
506 default = "redis+socket:///run/redis-sourcehut-metasrht/redis.sock?virtual_host=1";
507 };
508 welcome-emails = mkEnableOption (lib.mdDoc "sending stock sourcehut welcome emails after signup");
509 };
510 options."meta.sr.ht::api" = {
511 internal-ipnet = mkOption {
512 description = lib.mdDoc ''
513 Set of IP subnets which are permitted to utilize internal API
514 authentication. This should be limited to the subnets
515 from which your *.sr.ht services are running.
516 See [](#opt-services.sourcehut.listenAddress).
517 '';
518 type = with types; listOf str;
519 default = [ "127.0.0.0/8" "::1/128" ];
520 };
521 };
522 options."meta.sr.ht::aliases" = mkOption {
523 description = lib.mdDoc "Aliases for the client IDs of commonly used OAuth clients.";
524 type = with types; attrsOf int;
525 default = {};
526 example = { "git.sr.ht" = 12345; };
527 };
528 options."meta.sr.ht::billing" = {
529 enabled = mkEnableOption (lib.mdDoc "the billing system");
530 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
531 stripe-secret-key = mkOptionNullOrStr ''
532 An absolute file path (which should be outside the Nix-store)
533 to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
534 '' // {
535 apply = mapNullable (s: "<" + toString s);
536 };
537 };
538 options."meta.sr.ht::settings" = {
539 registration = mkEnableOption (lib.mdDoc "public registration");
540 onboarding-redirect = mkOption {
541 description = lib.mdDoc "Where to redirect new users upon registration.";
542 type = types.str;
543 default = "https://meta.localhost.localdomain";
544 };
545 user-invites = mkOption {
546 description = lib.mdDoc ''
547 How many invites each user is issued upon registration
548 (only applicable if open registration is disabled).
549 '';
550 type = types.ints.unsigned;
551 default = 5;
552 };
553 };
554
555 options."pages.sr.ht" = commonServiceSettings "pages" // {
556 gemini-certs = mkOption {
557 description = lib.mdDoc ''
558 An absolute file path (which should be outside the Nix-store)
559 to Gemini certificates.
560 '';
561 type = with types; nullOr path;
562 default = null;
563 };
564 max-site-size = mkOption {
565 description = lib.mdDoc "Maximum size of any given site (post-gunzip), in MiB.";
566 type = types.int;
567 default = 1024;
568 };
569 user-domain = mkOption {
570 description = lib.mdDoc ''
571 Configures the user domain, if enabled.
572 All users are given \<username\>.this.domain.
573 '';
574 type = with types; nullOr str;
575 default = null;
576 };
577 };
578 options."pages.sr.ht::api" = {
579 internal-ipnet = mkOption {
580 description = lib.mdDoc ''
581 Set of IP subnets which are permitted to utilize internal API
582 authentication. This should be limited to the subnets
583 from which your *.sr.ht services are running.
584 See [](#opt-services.sourcehut.listenAddress).
585 '';
586 type = with types; listOf str;
587 default = [ "127.0.0.0/8" "::1/128" ];
588 };
589 };
590
591 options."paste.sr.ht" = commonServiceSettings "paste" // {
592 };
593
594 options."todo.sr.ht" = commonServiceSettings "todo" // {
595 notify-from = mkOption {
596 description = lib.mdDoc "Outgoing email for notifications generated by users.";
597 type = types.str;
598 default = "todo-notify@localhost.localdomain";
599 };
600 webhooks = mkOption {
601 description = lib.mdDoc "The Redis connection used for the webhooks worker.";
602 type = types.str;
603 default = "redis+socket:///run/redis-sourcehut-todosrht/redis.sock?virtual_host=1";
604 };
605 };
606 options."todo.sr.ht::mail" = {
607 posting-domain = mkOption {
608 description = lib.mdDoc "Posting domain.";
609 type = types.str;
610 default = "todo.localhost.localdomain";
611 };
612 sock = mkOption {
613 description = lib.mdDoc ''
614 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
615 Alternatively, specify IP:PORT and an SMTP server will be run instead.
616 '';
617 type = types.str;
618 default = "/tmp/todo.sr.ht-lmtp.sock";
619 };
620 sock-group = mkOption {
621 description = lib.mdDoc ''
622 The lmtp daemon will make the unix socket group-read/write
623 for users in this group.
624 '';
625 type = types.str;
626 default = "postfix";
627 };
628 };
629 };
630 default = { };
631 description = lib.mdDoc ''
632 The configuration for the sourcehut network.
633 '';
634 };
635
636 builds = {
637 enableWorker = mkEnableOption (lib.mdDoc ''
638 worker for builds.sr.ht
639
640 ::: {.warning}
641 For smaller deployments, job runners can be installed alongside the master server
642 but even if you only build your own software, integration with other services
643 may cause you to run untrusted builds
644 (e.g. automatic testing of patches via listssrht).
645 See <https://man.sr.ht/builds.sr.ht/configuration.md#security-model>.
646 :::
647 '');
648
649 images = mkOption {
650 type = with types; attrsOf (attrsOf (attrsOf package));
651 default = { };
652 example = lib.literalExpression ''(let
653 # Pinning unstable to allow usage with flakes and limit rebuilds.
654 pkgs_unstable = builtins.fetchGit {
655 url = "https://github.com/NixOS/nixpkgs";
656 rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
657 ref = "nixos-unstable";
658 };
659 image_from_nixpkgs = (import ("''${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
660 pkgs = (import pkgs_unstable {});
661 });
662 in
663 {
664 nixos.unstable.x86_64 = image_from_nixpkgs;
665 }
666 )'';
667 description = lib.mdDoc ''
668 Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
669 '';
670 };
671 };
672
673 git = {
674 package = mkOption {
675 type = types.package;
676 default = pkgs.git;
677 defaultText = literalExpression "pkgs.git";
678 example = literalExpression "pkgs.gitFull";
679 description = lib.mdDoc ''
680 Git package for git.sr.ht. This can help silence collisions.
681 '';
682 };
683 fcgiwrap.preforkProcess = mkOption {
684 description = lib.mdDoc "Number of fcgiwrap processes to prefork.";
685 type = types.int;
686 default = 4;
687 };
688 };
689
690 hg = {
691 package = mkOption {
692 type = types.package;
693 default = pkgs.mercurial;
694 defaultText = literalExpression "pkgs.mercurial";
695 description = lib.mdDoc ''
696 Mercurial package for hg.sr.ht. This can help silence collisions.
697 '';
698 };
699 cloneBundles = mkOption {
700 type = types.bool;
701 default = false;
702 description = lib.mdDoc ''
703 Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
704 '';
705 };
706 };
707
708 lists = {
709 process = {
710 extraArgs = mkOption {
711 type = with types; listOf str;
712 default = [ "--loglevel DEBUG" "--pool eventlet" "--without-heartbeat" ];
713 description = lib.mdDoc "Extra arguments passed to the Celery responsible for processing mails.";
714 };
715 celeryConfig = mkOption {
716 type = types.lines;
717 default = "";
718 description = lib.mdDoc "Content of the `celeryconfig.py` used by the Celery of `listssrht-process`.";
719 };
720 };
721 };
722 };
723
724 config = mkIf cfg.enable (mkMerge [
725 {
726 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
727
728 services.sourcehut.settings = {
729 "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
730 "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
731 "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
732 "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
733 "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
734 "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
735 };
736 }
737 (mkIf cfg.postgresql.enable {
738 assertions = [
739 { assertion = postgresql.enable;
740 message = "postgresql must be enabled and configured";
741 }
742 ];
743 })
744 (mkIf cfg.postfix.enable {
745 assertions = [
746 { assertion = postfix.enable;
747 message = "postfix must be enabled and configured";
748 }
749 ];
750 # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
751 systemd.services.postfix.serviceConfig.PrivateTmp = true;
752 })
753 (mkIf cfg.redis.enable {
754 services.redis.vmOverCommit = mkDefault true;
755 })
756 (mkIf cfg.nginx.enable {
757 assertions = [
758 { assertion = nginx.enable;
759 message = "nginx must be enabled and configured";
760 }
761 ];
762 # For proxyPass= in virtual-hosts for Sourcehut services.
763 services.nginx.recommendedProxySettings = mkDefault true;
764 })
765 (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
766 services.openssh = {
767 # Note that sshd will continue to honor AuthorizedKeysFile.
768 # Note that you may want automatically rotate
769 # or link to /dev/null the following log files:
770 # - /var/log/gitsrht-dispatch
771 # - /var/log/{build,git,hg}srht-keys
772 # - /var/log/{git,hg}srht-shell
773 # - /var/log/gitsrht-update-hook
774 authorizedKeysCommand = ''/etc/ssh/sourcehut/subdir/srht-dispatch "%u" "%h" "%t" "%k"'';
775 # srht-dispatch will setuid/setgid according to [git.sr.ht::dispatch]
776 authorizedKeysCommandUser = "root";
777 extraConfig = ''
778 PermitUserEnvironment SRHT_*
779 '';
780 startWhenNeeded = false;
781 };
782 environment.etc."ssh/sourcehut/config.ini".source =
783 settingsFormat.generate "sourcehut-dispatch-config.ini"
784 (filterAttrs (k: v: k == "git.sr.ht::dispatch")
785 cfg.settings);
786 environment.etc."ssh/sourcehut/subdir/srht-dispatch" = {
787 # sshd_config(5): The program must be owned by root, not writable by group or others
788 mode = "0755";
789 source = pkgs.writeShellScript "srht-dispatch-wrapper" ''
790 set -e
791 set -x
792 cd /etc/ssh/sourcehut/subdir
793 ${pkgs.sourcehut.gitsrht}/bin/gitsrht-dispatch "$@"
794 '';
795 };
796 systemd.tmpfiles.settings."10-sourcehut-gitsrht" = mkIf cfg.git.enable (
797 builtins.listToAttrs (map (name: {
798 name = "/var/log/sourcehut/gitsrht-${name}";
799 value.f = {
800 inherit (cfg.git) user group;
801 mode = "0644";
802 };
803 }) [ "keys" "shell" "update-hook" ])
804 );
805 systemd.services.sshd = {
806 preStart = mkIf cfg.hg.enable ''
807 chown ${cfg.hg.user}:${cfg.hg.group} /var/log/sourcehut/hgsrht-keys
808 '';
809 serviceConfig = {
810 LogsDirectory = "sourcehut";
811 BindReadOnlyPaths =
812 # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
813 # for instance to get the user from the [git.sr.ht::dispatch] settings.
814 # *srht-keys needs to:
815 # - access a redis-server in [sr.ht] redis-host,
816 # - access the PostgreSQL server in [*.sr.ht] connection-string,
817 # - query metasrht-api (through the HTTP API).
818 # Using this has the side effect of creating empty files in /usr/bin/
819 optionals cfg.builds.enable [
820 "${pkgs.writeShellScript "buildsrht-keys-wrapper" ''
821 set -e
822 cd /run/sourcehut/buildsrht/subdir
823 exec -a "$0" ${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys "$@"
824 ''}:/usr/bin/buildsrht-keys"
825 "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
826 "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
827 ] ++
828 optionals cfg.git.enable [
829 # /path/to/gitsrht-keys calls /path/to/gitsrht-shell,
830 # or [git.sr.ht] shell= if set.
831 "${pkgs.writeShellScript "gitsrht-keys-wrapper" ''
832 set -e
833 cd /run/sourcehut/gitsrht/subdir
834 exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys "$@"
835 ''}:/usr/bin/gitsrht-keys"
836 "${pkgs.writeShellScript "gitsrht-shell-wrapper" ''
837 set -e
838 cd /run/sourcehut/gitsrht/subdir
839 export PATH="${cfg.git.package}/bin:$PATH"
840 export SRHT_CONFIG=/run/sourcehut/gitsrht/config.ini
841 exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell "$@"
842 ''}:/usr/bin/gitsrht-shell"
843 "${pkgs.writeShellScript "gitsrht-update-hook" ''
844 set -e
845 export SRHT_CONFIG=/run/sourcehut/gitsrht/config.ini
846 # hooks/post-update calls /usr/bin/gitsrht-update-hook as hooks/stage-3
847 # but this wrapper being a bash script, it overrides $0 with /usr/bin/gitsrht-update-hook
848 # hence this hack to put hooks/stage-3 back into gitsrht-update-hook's $0
849 if test "''${STAGE3:+set}"
850 then
851 exec -a hooks/stage-3 ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
852 else
853 export STAGE3=set
854 exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
855 fi
856 ''}:/usr/bin/gitsrht-update-hook"
857 ] ++
858 optionals cfg.hg.enable [
859 # /path/to/hgsrht-keys calls /path/to/hgsrht-shell,
860 # or [hg.sr.ht] shell= if set.
861 "${pkgs.writeShellScript "hgsrht-keys-wrapper" ''
862 set -e
863 cd /run/sourcehut/hgsrht/subdir
864 exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys "$@"
865 ''}:/usr/bin/hgsrht-keys"
866 "${pkgs.writeShellScript "hgsrht-shell-wrapper" ''
867 set -e
868 cd /run/sourcehut/hgsrht/subdir
869 exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell "$@"
870 ''}:/usr/bin/hgsrht-shell"
871 # Mercurial's changegroup hooks are run relative to their repository's directory,
872 # but hgsrht-hook-changegroup looks up ./config.ini
873 "${pkgs.writeShellScript "hgsrht-hook-changegroup" ''
874 set -e
875 test -e "''$PWD"/config.ini ||
876 ln -s /run/sourcehut/hgsrht/config.ini "''$PWD"/config.ini
877 exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-hook-changegroup "$@"
878 ''}:/usr/bin/hgsrht-hook-changegroup"
879 ];
880 };
881 };
882 })
883 ]);
884
885 imports = [
886
887 (import ./service.nix "builds" {
888 inherit configIniOfService;
889 srvsrht = "buildsrht";
890 port = 5002;
891 extraServices.buildsrht-api = {
892 serviceConfig.Restart = "always";
893 serviceConfig.RestartSec = "5s";
894 serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/buildsrht-api -b ${cfg.listenAddress}:${toString (cfg.builds.port + 100)}";
895 };
896 # TODO: a celery worker on the master and worker are apparently needed
897 extraServices.buildsrht-worker = let
898 qemuPackage = pkgs.qemu_kvm;
899 serviceName = "buildsrht-worker";
900 statePath = "/var/lib/sourcehut/${serviceName}";
901 in mkIf cfg.builds.enableWorker {
902 path = [ pkgs.openssh pkgs.docker ];
903 preStart = ''
904 set -x
905 if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
906 || test "$(cat ${statePath}/docker-image-qemu)" != "${qemuPackage.version}"
907 then
908 # Create and import qemu:latest image for docker
909 ${pkgs.dockerTools.streamLayeredImage {
910 name = "qemu";
911 tag = "latest";
912 contents = [ qemuPackage ];
913 }} | docker load
914 # Mark down current package version
915 echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
916 fi
917 '';
918 serviceConfig = {
919 ExecStart = "${pkgs.sourcehut.buildsrht}/bin/buildsrht-worker";
920 BindPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
921 LogsDirectory = [ "sourcehut/${serviceName}" ];
922 RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
923 StateDirectory = [ "sourcehut/${serviceName}" ];
924 TimeoutStartSec = "1800s";
925 # buildsrht-worker looks up ../config.ini
926 WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
927 };
928 };
929 extraConfig = let
930 image_dirs = flatten (
931 mapAttrsToList (distro: revs:
932 mapAttrsToList (rev: archs:
933 mapAttrsToList (arch: image:
934 pkgs.runCommand "buildsrht-images" { } ''
935 mkdir -p $out/${distro}/${rev}/${arch}
936 ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
937 ''
938 ) archs
939 ) revs
940 ) cfg.builds.images
941 );
942 image_dir_pre = pkgs.symlinkJoin {
943 name = "buildsrht-worker-images-pre";
944 paths = image_dirs;
945 # FIXME: not working, apparently because ubuntu/latest is a broken link
946 # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
947 };
948 image_dir = pkgs.runCommand "buildsrht-worker-images" { } ''
949 mkdir -p $out/images
950 cp -Lr ${image_dir_pre}/* $out/images
951 '';
952 in mkMerge [
953 {
954 users.users.${cfg.builds.user}.shell = pkgs.bash;
955
956 virtualisation.docker.enable = true;
957
958 services.sourcehut.settings = mkMerge [
959 { # Note that git.sr.ht::dispatch is not a typo,
960 # gitsrht-dispatch always use this section
961 "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
962 mkDefault "${cfg.builds.user}:${cfg.builds.group}";
963 }
964 (mkIf cfg.builds.enableWorker {
965 "builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
966 "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
967 "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
968 })
969 ];
970 }
971 (mkIf cfg.builds.enableWorker {
972 users.groups = {
973 docker.members = [ cfg.builds.user ];
974 };
975 })
976 (mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
977 # Allow nginx access to buildlogs
978 users.users.${nginx.user}.extraGroups = [ cfg.builds.group ];
979 systemd.services.nginx = {
980 serviceConfig.BindReadOnlyPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
981 };
982 services.nginx.virtualHosts."logs.${domain}" = mkMerge [ {
983 /* FIXME: is a listen needed?
984 listen = with builtins;
985 # FIXME: not compatible with IPv6
986 let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
987 [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
988 */
989 locations."/logs/".alias = cfg.settings."builds.sr.ht::worker".buildlogs + "/";
990 } cfg.nginx.virtualHost ];
991 })
992 ];
993 })
994
995 (import ./service.nix "git" (let
996 baseService = {
997 path = [ cfg.git.package ];
998 serviceConfig.BindPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
999 };
1000 in {
1001 inherit configIniOfService;
1002 mainService = mkMerge [ baseService {
1003 serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
1004 preStart = mkIf (versionOlder config.system.stateVersion "22.05") (mkBefore ''
1005 # Fix Git hooks of repositories pre-dating https://github.com/NixOS/nixpkgs/pull/133984
1006 (
1007 set +f
1008 shopt -s nullglob
1009 for h in /var/lib/sourcehut/gitsrht/repos/~*/*/hooks/{pre-receive,update,post-update}
1010 do ln -fnsv /usr/bin/gitsrht-update-hook "$h"; done
1011 )
1012 '');
1013 } ];
1014 port = 5001;
1015 webhooks = true;
1016 extraTimers.gitsrht-periodic = {
1017 service = baseService;
1018 timerConfig.OnCalendar = ["*:0/20"];
1019 };
1020 extraConfig = mkMerge [
1021 {
1022 # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
1023 # Probably could use gitsrht-shell if output is restricted to just parameters...
1024 users.users.${cfg.git.user}.shell = pkgs.bash;
1025 services.sourcehut.settings = {
1026 "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
1027 mkDefault "${cfg.git.user}:${cfg.git.group}";
1028 };
1029 systemd.services.sshd = baseService;
1030 }
1031 (mkIf cfg.nginx.enable {
1032 services.nginx.virtualHosts."git.${domain}" = {
1033 locations."/authorize" = {
1034 proxyPass = "http://${cfg.listenAddress}:${toString cfg.git.port}";
1035 extraConfig = ''
1036 proxy_pass_request_body off;
1037 proxy_set_header Content-Length "";
1038 proxy_set_header X-Original-URI $request_uri;
1039 '';
1040 };
1041 locations."~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$" = {
1042 root = "/var/lib/sourcehut/gitsrht/repos";
1043 fastcgiParams = {
1044 GIT_HTTP_EXPORT_ALL = "";
1045 GIT_PROJECT_ROOT = "$document_root";
1046 PATH_INFO = "$uri";
1047 SCRIPT_FILENAME = "${cfg.git.package}/bin/git-http-backend";
1048 };
1049 extraConfig = ''
1050 auth_request /authorize;
1051 fastcgi_read_timeout 500s;
1052 fastcgi_pass unix:/run/gitsrht-fcgiwrap.sock;
1053 gzip off;
1054 '';
1055 };
1056 };
1057 systemd.sockets.gitsrht-fcgiwrap = {
1058 before = [ "nginx.service" ];
1059 wantedBy = [ "sockets.target" "gitsrht.service" ];
1060 # This path remains accessible to nginx.service, which has no RootDirectory=
1061 socketConfig.ListenStream = "/run/gitsrht-fcgiwrap.sock";
1062 socketConfig.SocketUser = nginx.user;
1063 socketConfig.SocketMode = "600";
1064 };
1065 })
1066 ];
1067 extraServices.gitsrht-api.serviceConfig = {
1068 Restart = "always";
1069 RestartSec = "5s";
1070 ExecStart = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-api -b ${cfg.listenAddress}:${toString (cfg.git.port + 100)}";
1071 BindPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
1072 };
1073 extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
1074 serviceConfig = {
1075 # Socket is passed by gitsrht-fcgiwrap.socket
1076 ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${toString cfg.git.fcgiwrap.preforkProcess}";
1077 # No need for config.ini
1078 ExecStartPre = mkForce [];
1079 User = null;
1080 DynamicUser = true;
1081 BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
1082 IPAddressDeny = "any";
1083 InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis-sourcehut" ];
1084 PrivateNetwork = true;
1085 RestrictAddressFamilies = mkForce [ "none" ];
1086 SystemCallFilter = mkForce [
1087 "@system-service"
1088 "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid"
1089 # @timer is needed for alarm()
1090 ];
1091 };
1092 };
1093 }))
1094
1095 (import ./service.nix "hg" (let
1096 baseService = {
1097 path = [ cfg.hg.package ];
1098 serviceConfig.BindPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos" ];
1099 };
1100 in {
1101 inherit configIniOfService;
1102 mainService = mkMerge [ baseService {
1103 serviceConfig.StateDirectory = [ "sourcehut/hgsrht" "sourcehut/hgsrht/repos" ];
1104 } ];
1105 port = 5010;
1106 webhooks = true;
1107 extraTimers.hgsrht-periodic = {
1108 service = baseService;
1109 timerConfig.OnCalendar = ["*:0/20"];
1110 };
1111 extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
1112 service = baseService;
1113 timerConfig.OnCalendar = ["daily"];
1114 timerConfig.AccuracySec = "1h";
1115 };
1116 extraServices.hgsrht-api = {
1117 serviceConfig.Restart = "always";
1118 serviceConfig.RestartSec = "5s";
1119 serviceConfig.ExecStart = "${pkgs.sourcehut.hgsrht}/bin/hgsrht-api -b ${cfg.listenAddress}:${toString (cfg.hg.port + 100)}";
1120 };
1121 extraConfig = mkMerge [
1122 {
1123 users.users.${cfg.hg.user}.shell = pkgs.bash;
1124 services.sourcehut.settings = {
1125 # Note that git.sr.ht::dispatch is not a typo,
1126 # gitsrht-dispatch always uses this section.
1127 "git.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
1128 mkDefault "${cfg.hg.user}:${cfg.hg.group}";
1129 };
1130 systemd.services.sshd = baseService;
1131 }
1132 (mkIf cfg.nginx.enable {
1133 # Allow nginx access to repositories
1134 users.users.${nginx.user}.extraGroups = [ cfg.hg.group ];
1135 services.nginx.virtualHosts."hg.${domain}" = {
1136 locations."/authorize" = {
1137 proxyPass = "http://${cfg.listenAddress}:${toString cfg.hg.port}";
1138 extraConfig = ''
1139 proxy_pass_request_body off;
1140 proxy_set_header Content-Length "";
1141 proxy_set_header X-Original-URI $request_uri;
1142 '';
1143 };
1144 # Let clients reach pull bundles. We don't really need to lock this down even for
1145 # private repos because the bundles are named after the revision hashes...
1146 # so someone would need to know or guess a SHA value to download anything.
1147 # TODO: proxyPass to an hg serve service?
1148 locations."~ ^/[~^][a-z0-9_]+/[a-zA-Z0-9_.-]+/\\.hg/bundles/.*$" = {
1149 root = "/var/lib/nginx/hgsrht/repos";
1150 extraConfig = ''
1151 auth_request /authorize;
1152 gzip off;
1153 '';
1154 };
1155 };
1156 systemd.services.nginx = {
1157 serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/nginx/hgsrht/repos" ];
1158 };
1159 })
1160 ];
1161 }))
1162
1163 (import ./service.nix "hub" {
1164 inherit configIniOfService;
1165 port = 5014;
1166 extraConfig = {
1167 services.nginx = mkIf cfg.nginx.enable {
1168 virtualHosts."hub.${domain}" = mkMerge [ {
1169 serverAliases = [ domain ];
1170 } cfg.nginx.virtualHost ];
1171 };
1172 };
1173 })
1174
1175 (import ./service.nix "lists" (let
1176 srvsrht = "listssrht";
1177 in {
1178 inherit configIniOfService;
1179 port = 5006;
1180 webhooks = true;
1181 extraServices.listssrht-api = {
1182 serviceConfig.Restart = "always";
1183 serviceConfig.RestartSec = "5s";
1184 serviceConfig.ExecStart = "${pkgs.sourcehut.listssrht}/bin/listssrht-api -b ${cfg.listenAddress}:${toString (cfg.lists.port + 100)}";
1185 };
1186 # Receive the mail from Postfix and enqueue them into Redis and PostgreSQL
1187 extraServices.listssrht-lmtp = {
1188 wants = [ "postfix.service" ];
1189 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1190 serviceConfig.ExecStart = "${pkgs.sourcehut.listssrht}/bin/listssrht-lmtp";
1191 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1192 serviceConfig.PrivateUsers = mkForce false;
1193 };
1194 # Dequeue the mails from Redis and dispatch them
1195 extraServices.listssrht-process = {
1196 serviceConfig = {
1197 preStart = ''
1198 cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" cfg.lists.process.celeryConfig} \
1199 /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
1200 '';
1201 ExecStart = "${cfg.python}/bin/celery --app listssrht.process worker --hostname listssrht-process@%%h " + concatStringsSep " " cfg.lists.process.extraArgs;
1202 # Avoid crashing: os.getloadavg()
1203 ProcSubset = mkForce "all";
1204 };
1205 };
1206 extraConfig = mkIf cfg.postfix.enable {
1207 users.groups.${postfix.group}.members = [ cfg.lists.user ];
1208 services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
1209 services.postfix = {
1210 destination = [ "lists.${domain}" ];
1211 # FIXME: an accurate recipient list should be queried
1212 # from the lists.sr.ht PostgreSQL database to avoid backscattering.
1213 # But usernames are unfortunately not in that database but in meta.sr.ht.
1214 # Note that two syntaxes are allowed:
1215 # - ~username/list-name@lists.${domain}
1216 # - u.username.list-name@lists.${domain}
1217 localRecipients = [ "@lists.${domain}" ];
1218 transport = ''
1219 lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
1220 '';
1221 };
1222 };
1223 }))
1224
1225 (import ./service.nix "man" {
1226 inherit configIniOfService;
1227 port = 5004;
1228 })
1229
1230 (import ./service.nix "meta" {
1231 inherit configIniOfService;
1232 port = 5000;
1233 webhooks = true;
1234 extraTimers.metasrht-daily.timerConfig = {
1235 OnCalendar = ["daily"];
1236 AccuracySec = "1h";
1237 };
1238 extraServices.metasrht-api = {
1239 serviceConfig.Restart = "always";
1240 serviceConfig.RestartSec = "5s";
1241 preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
1242 let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
1243 srv = head srvMatch;
1244 in
1245 # Configure client(s) as "preauthorized"
1246 optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
1247 # Configure ${srv}'s OAuth client as "preauthorized"
1248 ${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
1249 -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
1250 ''
1251 ) cfg.settings));
1252 serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
1253 };
1254 extraConfig = {
1255 assertions = [
1256 { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
1257 s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
1258 message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
1259 }
1260 ];
1261 environment.systemPackages = optional cfg.meta.enable
1262 (pkgs.writeShellScriptBin "metasrht-manageuser" ''
1263 set -eux
1264 if test "$(${pkgs.coreutils}/bin/id -n -u)" != '${cfg.meta.user}'
1265 then exec sudo -u '${cfg.meta.user}' "$0" "$@"
1266 else
1267 # In order to load config.ini
1268 if cd /run/sourcehut/metasrht
1269 then exec ${pkgs.sourcehut.metasrht}/bin/metasrht-manageuser "$@"
1270 else cat <<EOF
1271 Please run: sudo systemctl start metasrht
1272 EOF
1273 exit 1
1274 fi
1275 fi
1276 '');
1277 };
1278 })
1279
1280 (import ./service.nix "pages" {
1281 inherit configIniOfService;
1282 port = 5112;
1283 mainService = let
1284 srvsrht = "pagessrht";
1285 version = pkgs.sourcehut.${srvsrht}.version;
1286 stateDir = "/var/lib/sourcehut/${srvsrht}";
1287 iniKey = "pages.sr.ht";
1288 in {
1289 preStart = mkBefore ''
1290 set -x
1291 # Use the /run/sourcehut/${srvsrht}/config.ini
1292 # installed by a previous ExecStartPre= in baseService
1293 cd /run/sourcehut/${srvsrht}
1294
1295 if test ! -e ${stateDir}/db; then
1296 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
1297 echo ${version} >${stateDir}/db
1298 fi
1299
1300 ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
1301 # Just try all the migrations because they're not linked to the version
1302 for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
1303 ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
1304 done
1305 ''}
1306
1307 # Disable webhook
1308 touch ${stateDir}/webhook
1309 '';
1310 serviceConfig = {
1311 ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
1312 };
1313 };
1314 })
1315
1316 (import ./service.nix "paste" {
1317 inherit configIniOfService;
1318 port = 5011;
1319 })
1320
1321 (import ./service.nix "todo" {
1322 inherit configIniOfService;
1323 port = 5003;
1324 webhooks = true;
1325 extraServices.todosrht-api = {
1326 serviceConfig.Restart = "always";
1327 serviceConfig.RestartSec = "5s";
1328 serviceConfig.ExecStart = "${pkgs.sourcehut.todosrht}/bin/todosrht-api -b ${cfg.listenAddress}:${toString (cfg.todo.port + 100)}";
1329 };
1330 extraServices.todosrht-lmtp = {
1331 wants = [ "postfix.service" ];
1332 unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
1333 serviceConfig.ExecStart = "${pkgs.sourcehut.todosrht}/bin/todosrht-lmtp";
1334 # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
1335 serviceConfig.PrivateUsers = mkForce false;
1336 };
1337 extraConfig = mkIf cfg.postfix.enable {
1338 users.groups.${postfix.group}.members = [ cfg.todo.user ];
1339 services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
1340 services.postfix = {
1341 destination = [ "todo.${domain}" ];
1342 # FIXME: an accurate recipient list should be queried
1343 # from the todo.sr.ht PostgreSQL database to avoid backscattering.
1344 # But usernames are unfortunately not in that database but in meta.sr.ht.
1345 # Note that two syntaxes are allowed:
1346 # - ~username/tracker-name@todo.${domain}
1347 # - u.username.tracker-name@todo.${domain}
1348 localRecipients = [ "@todo.${domain}" ];
1349 transport = ''
1350 todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
1351 '';
1352 };
1353 };
1354 })
1355
1356 (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
1357 [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
1358 (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
1359 [ "services" "sourcehut" "listenAddress" ])
1360
1361 (mkRemovedOptionModule [ "services" "sourcehut" "dispatch" ] ''
1362 dispatch is deprecated. See https://sourcehut.org/blog/2022-08-01-dispatch-deprecation-plans/
1363 for more information.
1364 '')
1365
1366 (mkRemovedOptionModule [ "services" "sourcehut" "services"] ''
1367 This option was removed in favor of individual <service>.enable flags.
1368 '')
1369 ];
1370
1371 meta.doc = ./default.md;
1372 meta.maintainers = with maintainers; [ tomberek ];
1373}