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