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